Java 11 was released in September 2018 as the first Long-Term Support (LTS) release after Java 8. This makes it one of the most important upgrade targets in Java history. If your organization skipped Java 9 and 10 (as most did), migrating from Java 8 to Java 11 means dealing with three major versions of changes at once — the module system (Java 9), API removals, deprecated features, and new defaults.
The good news: millions of projects have made this migration successfully. The bad news: it is not a simple recompile. Java 11 removed several APIs that were bundled with the JDK since the early days, and the module system changes how the classpath and class loading work. If you do not prepare, you will hit compilation errors and runtime exceptions that did not exist in Java 8.
This guide covers everything that was removed, deprecated, or changed, and provides the exact replacements and fixes you need. Here is a high-level overview:
| Category | What Changed | Impact Level |
|---|---|---|
| Java EE Modules | JAXB, JAX-WS, CORBA, javax.activation, javax.annotation removed | High — breaks most enterprise apps |
| JavaFX | Removed from JDK, now a separate project (OpenJFX) | High for desktop apps |
| Nashorn JavaScript | Deprecated (removed in Java 15) | Medium — affects apps embedding JS |
| Module System | JPMS (Java 9) affects classpath, reflection, internal APIs | High — affects most large apps |
| Deprecated APIs | Pack200, Applet API, SecurityManager | Low-Medium |
| Build Tools | Maven/Gradle plugins need updates | Medium |
Target audience: This guide is for teams migrating from Java 8 to Java 11. If you are on Java 9 or 10, many of these changes are already familiar, but the removal of Java EE modules (which were only deprecated in Java 9) is new in Java 11.
This is the change that breaks the most Java 8 applications. Java has shipped with several Java EE APIs since Java 6, bundled as part of the JDK itself. In Java 9, these modules were deprecated. In Java 11, they were completely removed. If your code uses any of these APIs, it will fail with ClassNotFoundException or NoClassDefFoundError on Java 11.
JAXB (Java Architecture for XML Binding) marshals Java objects to XML and back. It was incredibly common in SOAP web services and enterprise applications. If your code has import javax.xml.bind.*, it will break on Java 11.
Maven replacement:
jakarta.xml.bind jakarta.xml.bind-api 4.0.2 org.glassfish.jaxb jaxb-runtime 4.0.5 runtime
Note on the Jakarta namespace: The Java EE APIs have been transferred to the Eclipse Foundation under the Jakarta EE umbrella. Newer versions use the jakarta.* package prefix instead of javax.*. If you need to maintain the old javax.xml.bind package names for compatibility with existing code, use the older 2.x versions:
javax.xml.bind jaxb-api 2.3.1 org.glassfish.jaxb jaxb-runtime 2.3.9 runtime
Code example — JAXB works the same, just needs the dependency:
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.StringWriter;
@XmlRootElement
public class Employee {
private String name;
private String department;
// Default constructor required by JAXB
public Employee() {}
public Employee(String name, String department) {
this.name = name;
this.department = department;
}
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDepartment() { return department; }
public void setDepartment(String department) { this.department = department; }
public static void main(String[] args) throws Exception {
Employee emp = new Employee("Alice", "Engineering");
JAXBContext context = JAXBContext.newInstance(Employee.class);
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
StringWriter writer = new StringWriter();
marshaller.marshal(emp, writer);
System.out.println(writer.toString());
}
}
// Output (after adding JAXB dependency):
//
//
// Engineering
// Alice
//
JAX-WS was the API for SOAP web services. If your application generates SOAP clients from WSDL files or provides SOAP endpoints, you need this replacement.
Maven replacement:
jakarta.xml.ws jakarta.xml.ws-api 4.0.2 com.sun.xml.ws jaxws-rt 4.0.3 runtime
The Activation Framework is used for MIME type handling and is a dependency of JAXB and JavaMail. Even if you do not use it directly, you may need it as a transitive dependency.
Maven replacement:
jakarta.activation jakarta.activation-api 2.1.3
This module provided annotations like @PostConstruct, @PreDestroy, @Resource, and @Generated. These are heavily used in Spring, CDI, and other dependency injection frameworks.
Maven replacement:
jakarta.annotation jakarta.annotation-api 3.0.0 javax.annotation javax.annotation-api 1.3.2
CORBA (Common Object Request Broker Architecture) was a 1990s-era technology for distributed computing. It was removed entirely from Java 11, and there is no standalone replacement because the technology is essentially dead. If you have legacy CORBA code, your options are limited to staying on Java 8 for that component or migrating to REST/gRPC.
| Removed Module | Package | What It Did | Maven Replacement (groupId:artifactId) |
|---|---|---|---|
java.xml.bind |
javax.xml.bind |
XML-to-Java object binding (JAXB) | jakarta.xml.bind:jakarta.xml.bind-api + org.glassfish.jaxb:jaxb-runtime |
java.xml.ws |
javax.xml.ws |
SOAP web services (JAX-WS) | jakarta.xml.ws:jakarta.xml.ws-api + com.sun.xml.ws:jaxws-rt |
java.xml.ws.annotation |
javax.annotation |
Common annotations (@PostConstruct, etc.) | jakarta.annotation:jakarta.annotation-api |
java.activation |
javax.activation |
MIME type handling | jakarta.activation:jakarta.activation-api |
java.corba |
org.omg.CORBA |
Distributed object computing | None — technology is obsolete |
java.transaction |
javax.transaction |
JTA (Java Transaction API) | jakarta.transaction:jakarta.transaction-api |
JavaFX, the modern GUI toolkit that was meant to replace Swing, was removed from the JDK starting with Java 11. It now lives as an independent open-source project called OpenJFX.
If your application uses JavaFX, you need to add it as an explicit dependency:
org.openjfx javafx-controls 21.0.5 org.openjfx javafx-fxml 21.0.5 org.openjfx javafx-maven-plugin 0.0.8 com.example.App
For Gradle:
// build.gradle
plugins {
id 'org.openjfx.javafxplugin' version '0.1.0'
}
javafx {
version = "21.0.5"
modules = ['javafx.controls', 'javafx.fxml']
}
The OpenJFX project is actively maintained and continues to receive updates. Your existing JavaFX code should work with minimal changes — the main effort is adding the dependency and configuring the module path.
The Nashorn JavaScript engine (javax.script with engine name “nashorn”) was deprecated in Java 11 (JEP 335) and fully removed in Java 15. Nashorn was introduced in Java 8 as a replacement for the even older Rhino engine, but maintaining a full JavaScript engine inside the JDK proved impractical given how quickly JavaScript evolves.
Alternatives:
| Alternative | Description | Best For |
|---|---|---|
| GraalJS | JavaScript engine from GraalVM project | Drop-in replacement for Nashorn, modern ECMAScript support |
| J2V8 | Java bindings for Google V8 engine | Performance-critical JavaScript execution |
| Rhino | Mozilla’s JavaScript engine (standalone) | Legacy compatibility |
If you are using Nashorn to evaluate JavaScript expressions or run scripts, GraalJS is the recommended replacement:
org.graalvm.js js 23.0.3 org.graalvm.js js-scriptengine 23.0.3
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
public class JavaScriptExample {
public static void main(String[] args) throws Exception {
// With GraalJS on the classpath, this uses GraalJS instead of Nashorn
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("graal.js");
if (engine != null) {
Object result = engine.eval("1 + 2 + 3");
System.out.println("Result: " + result); // Result: 6
} else {
System.out.println("GraalJS engine not found. Add graaljs dependency.");
}
}
}
The java.se.ee module was an aggregator module that included all the Java EE modules listed above (JAXB, JAX-WS, CORBA, etc.). In Java 9, it was deprecated. In Java 11, it was removed along with all the modules it aggregated.
If your module-info.java contains requires java.se.ee;, remove it and add explicit dependencies for the specific modules you need (using the Maven dependencies shown in section 2).
These features were deprecated in Java 11 but not yet removed. They will be removed in future Java versions:
Pack200 was a compression scheme for JAR files. It was deprecated in Java 11 (JEP 336) and removed in Java 14. The pack200 and unpack200 command-line tools and the java.util.jar.Pack200 API are gone.
Impact: If your build process or deployment pipeline uses Pack200 compression, switch to standard ZIP/JAR compression or use jlink to create custom runtime images (which are smaller than compressed JARs anyway).
The Applet API (java.applet.Applet) was deprecated in Java 9 and carries a forRemoval=true annotation. Browsers dropped support for Java applets years ago. If you still have applet-based code, migrate to Java Web Start (also deprecated) or web-based alternatives.
SecurityManager was not deprecated in Java 11 itself, but it was deprecated for removal in Java 17 (JEP 411). If you rely on SecurityManager for sandboxing, start planning your migration now. Java is moving toward other security mechanisms (process isolation, containers, etc.).
The Java Platform Module System (JPMS), introduced in Java 9, is the biggest architectural change in Java’s history. While you do not have to modularize your application to run on Java 11, the module system still affects classpath-based applications in important ways.
If you do not create a module-info.java, your entire application runs in the unnamed module. The unnamed module can access all exported packages from all named modules, so most code works without changes. However:
// This worked in Java 8 but may not in Java 11: // Accessing internal JDK APIs import sun.misc.Unsafe; // Encapsulated in java.base import sun.misc.BASE64Encoder; // Removed entirely // Fix: Use the public API equivalents import java.util.Base64; // Replacement for BASE64Encoder/Decoder // For Unsafe, use VarHandle (Java 9+) or MethodHandles
If your application or its dependencies access internal JDK APIs, you may need these JVM flags:
| Flag | Purpose | Example |
|---|---|---|
--add-modules |
Add a module to the module graph | --add-modules java.sql |
--add-opens |
Open a package for deep reflection (setAccessible) | --add-opens java.base/java.lang=ALL-UNNAMED |
--add-exports |
Export a package to another module | --add-exports java.base/sun.nio.ch=ALL-UNNAMED |
--add-reads |
Add a read edge between modules | --add-reads mymodule=java.logging |
--illegal-access |
Control illegal reflective access (removed in Java 17) | --illegal-access=permit (Java 11-16 only) |
Many frameworks (Spring, Hibernate, Jackson) use deep reflection to access private fields. On Java 11, you may see warnings like:
// Warning you may see at runtime: // WARNING: An illegal reflective access operation has occurred // WARNING: Illegal reflective access by org.springframework.core.io.support... // WARNING: Please consider reporting this to the maintainers of org.springframework... // Fix: Add --add-opens flags to your JVM startup // For Spring Boot applications, add to your startup script or JAVA_OPTS: // java --add-opens java.base/java.lang=ALL-UNNAMED \ // --add-opens java.base/java.lang.reflect=ALL-UNNAMED \ // --add-opens java.base/java.util=ALL-UNNAMED \ // -jar myapp.jar // For Maven Surefire plugin (test execution): //// org.apache.maven.plugins //maven-surefire-plugin //3.2.5 //// //// --add-opens java.base/java.lang=ALL-UNNAMED // --add-opens java.base/java.util=ALL-UNNAMED // //
Best practice: Update your frameworks and libraries to versions that support Java 11 natively. Modern versions of Spring (5.1+), Hibernate (5.4+), and Jackson (2.10+) have been updated to avoid illegal reflective access.
Follow this step-by-step checklist when migrating from Java 8 to Java 11:
| Step | Action | Details |
|---|---|---|
| 1 | Inventory your dependencies | List all Maven/Gradle dependencies and check their Java 11 compatibility. Use mvn dependency:tree to see the full tree. |
| 2 | Check for Java EE API usage | Search your codebase for import javax.xml.bind, import javax.xml.ws, import javax.annotation. These will break. |
| 3 | Check for internal API usage | Search for import sun., import com.sun.. These are encapsulated in Java 11. |
| 4 | Update build tools | Maven 3.5.0+ and Gradle 5.0+ are minimum for Java 11 support. See section 10 for plugin versions. |
| 5 | Create a branch | Work on a dedicated branch. Keep the Java 8 version running in production until migration is validated. |
| Step | Action | Details |
|---|---|---|
| 6 | Set compiler target to 11 | Update maven-compiler-plugin source and target to 11. See section 10. |
| 7 | Add Java EE replacement dependencies | Add JAXB, JAX-WS, javax.annotation dependencies from section 2. |
| 8 | Fix compilation errors | Address removed API usage, internal API access, and deprecated method warnings. |
| 9 | Run the test suite | Fix any test failures. Pay attention to reflection-based tests and XML processing. |
| Step | Action | Details |
|---|---|---|
| 10 | Test with Java 11 JVM | Run the application and check for IllegalAccessError, ClassNotFoundException, or reflective access warnings. |
| 11 | Add --add-opens flags if needed |
For frameworks that use deep reflection, add the necessary module opens. |
| 12 | Test critical paths | Test XML processing, serialization, database access, and any integration points. |
| 13 | Performance test | Java 11 has improved garbage collectors (G1 is now default). Run performance benchmarks. |
| 14 | Deploy to staging | Run in a production-like environment for at least a week before production deployment. |
This table covers the most frequently encountered issues when migrating from Java 8 to Java 11:
| Issue | Error Message | Fix |
|---|---|---|
| JAXB missing | ClassNotFoundException: javax.xml.bind.JAXBContext |
Add jakarta.xml.bind:jakarta.xml.bind-api + org.glassfish.jaxb:jaxb-runtime |
| JAX-WS missing | ClassNotFoundException: javax.xml.ws.Service |
Add jakarta.xml.ws:jakarta.xml.ws-api + com.sun.xml.ws:jaxws-rt |
| @PostConstruct missing | ClassNotFoundException: javax.annotation.PostConstruct |
Add jakarta.annotation:jakarta.annotation-api |
| sun.misc.BASE64 removed | ClassNotFoundException: sun.misc.BASE64Encoder |
Replace with java.util.Base64.getEncoder() |
| sun.misc.Unsafe | IllegalAccessError |
Use --add-opens java.base/jdk.internal.misc=ALL-UNNAMED or migrate to VarHandle |
| Reflective access warning | WARNING: An illegal reflective access operation has occurred |
Update the library, or add --add-opens for the specific package |
| JavaFX missing | ClassNotFoundException: javafx.application.Application |
Add OpenJFX dependencies (see section 3) |
| Nashorn missing | ScriptEngine is null |
Add GraalJS dependency (see section 4) |
| Lombok compilation error | Various annotation processor errors | Update Lombok to 1.18.4+ (supports Java 11) |
| JaCoCo code coverage fails | IllegalArgumentException: Unsupported class file major version |
Update JaCoCo to 0.8.3+ |
| Mockito reflection error | InaccessibleObjectException |
Update Mockito to 2.23+ or add --add-opens |
| Spring Boot startup error | Various CGLIB/ASM errors | Update to Spring Boot 2.1+ (first version with full Java 11 support) |
This is one of the most common fixes. The old internal API was never part of the public specification:
import java.util.Base64;
public class Base64Migration {
public static void main(String[] args) {
String original = "Hello, Java 11!";
// --- BEFORE: Java 8 internal API (REMOVED in Java 11) ---
// sun.misc.BASE64Encoder encoder = new sun.misc.BASE64Encoder();
// String encoded = encoder.encode(original.getBytes());
// sun.misc.BASE64Decoder decoder = new sun.misc.BASE64Decoder();
// byte[] decoded = decoder.decodeBuffer(encoded);
// --- AFTER: Standard API (Java 8+) ---
// Encode
String encoded = Base64.getEncoder().encodeToString(original.getBytes());
System.out.println("Encoded: " + encoded);
// Decode
byte[] decodedBytes = Base64.getDecoder().decode(encoded);
String decoded = new String(decodedBytes);
System.out.println("Decoded: " + decoded);
// URL-safe encoding (for URLs and filenames)
String urlEncoded = Base64.getUrlEncoder().encodeToString(original.getBytes());
System.out.println("URL-safe: " + urlEncoded);
// MIME encoding (with line breaks every 76 chars)
String mimeEncoded = Base64.getMimeEncoder().encodeToString(
"A longer string that would benefit from MIME line wrapping".getBytes());
System.out.println("MIME: " + mimeEncoded);
}
}
// Output:
// Encoded: SGVsbG8sIEphdmEgMTEh
// Decoded: Hello, Java 11!
// URL-safe: SGVsbG8sIEphdmEgMTEh
// MIME: QSBsb25nZXIgc3RyaW5nIHRoYXQgd291bGQgYmVuZWZpdCBmcm9tIE1JTUUgbGluZSB3cmFw
// cGluZw==
Java provides the jdeps tool to scan your JARs for internal API usage. Run this before migrating to identify problems early:
// Command: scan a JAR for internal API dependencies // jdeps --jdk-internals myapp.jar // // Example output: // myapp.jar -> java.base // com.myapp.util.Encoder -> sun.misc.BASE64Encoder JDK internal API (removed) // com.myapp.security.Crypto -> sun.security.ssl.SSLSessionImpl JDK internal API // // Command: check module dependencies // jdeps --module-path libs --print-module-deps myapp.jar // // Example output: // java.base,java.logging,java.sql,java.xml
Your build tools and their plugins must be updated to support Java 11. Here are the minimum versions required:
| Component | Minimum Version | Recommended Version | Notes |
|---|---|---|---|
| Maven itself | 3.5.0 | 3.9.6+ | Older versions may not recognize Java 11 bytecode |
maven-compiler-plugin |
3.8.0 | 3.12.1+ | Required for release flag support |
maven-surefire-plugin |
2.22.0 | 3.2.5+ | Needed for Java 11 test execution |
maven-failsafe-plugin |
2.22.0 | 3.2.5+ | Integration test plugin |
maven-jar-plugin |
3.1.0 | 3.3.0+ | Correct manifest generation |
jacoco-maven-plugin |
0.8.3 | 0.8.11+ | Code coverage for Java 11 bytecode |
Maven compiler configuration for Java 11:
11 11 11 org.apache.maven.plugins maven-compiler-plugin 3.12.1 11 org.apache.maven.plugins maven-surefire-plugin 3.2.5 --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED
Why use release instead of source/target? The --release flag (Java 9+) is a single setting that simultaneously sets the source level, target level, and restricts the available APIs to those present in the specified Java version. Using source and target alone lets you accidentally use Java 17 APIs even when targeting Java 11, causing runtime errors on Java 11.
| Component | Minimum Version | Recommended Version |
|---|---|---|
| Gradle itself | 5.0 | 8.5+ |
| Java plugin | Built-in | N/A (use java or java-library plugin) |
Gradle build.gradle configuration for Java 11:
plugins {
id 'java'
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
// Or using toolchains (Gradle 6.7+, preferred approach):
java {
toolchain {
languageVersion = JavaLanguageVersion.of(11)
}
}
// Add module opens for tests if needed
tasks.withType(Test).configureEach {
jvmArgs(
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
'--add-opens', 'java.base/java.util=ALL-UNNAMED'
)
}
Here are the minimum library versions that support Java 11:
| Library | Minimum Java 11 Version | Notes |
|---|---|---|
| Spring Boot | 2.1.0 | Spring Framework 5.1+ required |
| Spring Framework | 5.1.0 | Full Java 11 support |
| Hibernate ORM | 5.4.0 | Earlier 5.x versions may work with --add-opens |
| Jackson | 2.10.0 | Full module system support |
| Lombok | 1.18.4 | Annotation processing updated for Java 11 |
| Mockito | 2.23.0 | Uses ByteBuddy which needs Java 11 support |
| JUnit 5 | 5.4.0 | JUnit 4 also works but consider migrating |
| Log4j 2 | 2.13.0 | Earlier versions have Java 11 issues |
| Guava | 27.0 | Module system support added |
| Apache HttpClient | 4.5.13 / 5.1 | Consider using Java 11’s built-in HttpClient instead |
If you build and deploy with Docker, update your base images:
// Dockerfile -- BEFORE (Java 8) // FROM openjdk:8-jdk-slim // COPY target/myapp.jar /app/myapp.jar // CMD ["java", "-jar", "/app/myapp.jar"] // Dockerfile -- AFTER (Java 11) // FROM eclipse-temurin:11-jdk-jammy // COPY target/myapp.jar /app/myapp.jar // CMD ["java", \ // "--add-opens", "java.base/java.lang=ALL-UNNAMED", \ // "-jar", "/app/myapp.jar"] // For production, use JRE instead of JDK: // FROM eclipse-temurin:11-jre-jammy // COPY target/myapp.jar /app/myapp.jar // CMD ["java", "-jar", "/app/myapp.jar"]
Here is a real-world example showing code that compiles on Java 8 but fails on Java 11, along with the corrected version:
import java.util.Base64;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;
/**
* This class demonstrates code AFTER migration to Java 11.
* Comments show what the Java 8 version looked like.
*/
public class MigrationExample {
// BEFORE (Java 8): Used sun.misc.BASE64Encoder
// private static String encodeBase64(byte[] data) {
// return new sun.misc.BASE64Encoder().encode(data);
// }
// AFTER (Java 11): Use java.util.Base64
private static String encodeBase64(byte[] data) {
return Base64.getEncoder().encodeToString(data);
}
// BEFORE (Java 8): Used javax.xml.bind for hex encoding
// private static String bytesToHex(byte[] bytes) {
// return javax.xml.bind.DatatypeConverter.printHexBinary(bytes);
// }
// AFTER (Java 11): Manual hex conversion or HexFormat (Java 17+)
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
// BEFORE (Java 8): Verbose file reading
// private static String readFile(String path) throws Exception {
// byte[] bytes = java.nio.file.Files.readAllBytes(
// java.nio.file.Paths.get(path));
// return new String(bytes, java.nio.charset.StandardCharsets.UTF_8);
// }
// AFTER (Java 11): One-line file reading
private static String readFile(String path) throws Exception {
return Files.readString(Path.of(path));
}
// BEFORE (Java 8): Verbose blank check
// private static boolean isNullOrBlank(String str) {
// return str == null || str.trim().isEmpty();
// }
// AFTER (Java 11): Using isBlank()
private static boolean isNullOrBlank(String str) {
return str == null || str.isBlank();
}
// BEFORE (Java 8): Collection to typed array
// private static String[] toArray(List list) {
// return list.toArray(new String[0]);
// }
// AFTER (Java 11): Cleaner toArray
private static String[] toArray(List list) {
return list.toArray(String[]::new);
}
public static void main(String[] args) throws Exception {
// Demonstrate all migrated methods
System.out.println("Base64: " + encodeBase64("Hello Java 11".getBytes()));
System.out.println("Hex: " + bytesToHex("Hi".getBytes()));
System.out.println("isNullOrBlank(null): " + isNullOrBlank(null));
System.out.println("isNullOrBlank(\" \"): " + isNullOrBlank(" "));
System.out.println("isNullOrBlank(\"hi\"): " + isNullOrBlank("hi"));
// Write and read a file using Java 11 APIs
Path tempFile = Path.of("migration-test.txt");
Files.writeString(tempFile, "Java 11 migration successful!");
System.out.println("File content: " + readFile("migration-test.txt"));
// Cleanup
Files.deleteIfExists(tempFile);
// Type-safe toArray
List names = List.of("Alice", "Bob", "Charlie");
String[] array = toArray(names);
System.out.println("Array: " + java.util.Arrays.toString(array));
}
}
// Output:
// Base64: SGVsbG8gSmF2YSAxMQ==
// Hex: 4869
// isNullOrBlank(null): true
// isNullOrBlank(" "): true
// isNullOrBlank("hi"): false
// File content: Java 11 migration successful!
// Array: [Alice, Bob, Charlie]
Java 11 made G1 (Garbage First) the default garbage collector, replacing the Parallel GC from Java 8. G1 is optimized for lower pause times at the cost of slightly lower throughput. For most applications, this is the right trade-off. Key differences:
| Feature | Parallel GC (Java 8 default) | G1 GC (Java 11 default) |
|---|---|---|
| Optimization goal | Maximum throughput | Balanced throughput and latency |
| Pause times | Can be long (seconds) | Predictable, short pauses (target: 200ms) |
| Heap size sweet spot | Small to medium heaps | Medium to large heaps (4GB+) |
| Best for | Batch processing, background jobs | Web servers, microservices, interactive apps |
If your application is a batch processor or throughput-critical system and you notice performance degradation after migration, you can switch back to the Parallel GC:
// Use Parallel GC (Java 8 behavior) // java -XX:+UseParallelGC -jar myapp.jar // Use G1 GC (Java 11 default, explicit for clarity) // java -XX:+UseG1GC -jar myapp.jar // Tune G1 pause target (default 200ms) // java -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar myapp.jar // Epsilon GC (no-op collector, Java 11+) -- for benchmarking only // java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -jar myapp.jar // ZGC (experimental in Java 11, production-ready in Java 15) // java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -jar myapp.jar
Java 9 introduced Compact Strings (enabled by default in Java 11) which stores Latin-1 strings (English, most European languages) using 1 byte per character instead of 2 bytes. This can reduce memory usage by 30-50% for string-heavy applications. This happens automatically — no code changes needed. If you suspect it causes issues (extremely rare), you can disable it with -XX:-CompactStrings.
Starting with Java 11, Oracle changed its licensing model. Oracle JDK is no longer free for commercial production use (though Oracle OpenJDK builds are). Many organizations switched to alternative distributions:
| Distribution | Vendor | Free for Production? | LTS Support |
|---|---|---|---|
| Eclipse Temurin (Adoptium) | Eclipse Foundation | Yes | Yes (community) |
| Amazon Corretto | Amazon | Yes | Yes (Amazon-backed) |
| Azul Zulu | Azul Systems | Yes (Community Edition) | Yes |
| Red Hat OpenJDK | Red Hat | Yes (with RHEL) | Yes |
| Oracle OpenJDK | Oracle | Yes | 6 months only |
| Oracle JDK | Oracle | No (requires license) | Yes (paid) |
Recommendation: For most teams, Eclipse Temurin (formerly AdoptOpenJDK) or Amazon Corretto are the best choices for free, production-ready Java 11 distributions with long-term support.
// 1. Check current Java version // java -version // 2. Scan JARs for internal API usage // jdeps --jdk-internals myapp.jar // jdeps --jdk-internals --multi-release 11 myapp.jar // 3. Find module dependencies // jdeps --print-module-deps myapp.jar // 4. Search codebase for removed APIs // grep -r "import javax.xml.bind" src/ // grep -r "import javax.xml.ws" src/ // grep -r "import javax.annotation" src/ // grep -r "import sun.misc" src/ // grep -r "import com.sun" src/ // 5. Check Maven dependency tree for conflicts // mvn dependency:tree // mvn dependency:analyze // 6. Run tests with Java 11 // mvn clean test -Dmaven.compiler.release=11 // 7. Create custom runtime image (optional, reduces deployment size) // jlink --add-modules java.base,java.sql,java.logging \ // --output custom-jre \ // --strip-debug \ // --compress 2
Final advice: The Java 8 to 11 migration is a significant effort, but it unlocks access to three years of Java improvements: the module system, new language features (var, improved try-with-resources), new APIs (HttpClient, new String methods, new file I/O), and better performance (G1 GC as default, compact strings, improved JIT compiler). The investment pays for itself quickly, especially since Java 11 is an LTS release supported until at least 2026 (extended support through 2032 from some vendors). Do not wait — the longer you stay on Java 8, the harder the eventual migration becomes.