Here’s a quick, no-fluff rundown of the new features landing in Java 25.
JDK 25 packs 18 new features: 12 delivered, 4 in preview, 1 experimental, and 1 incubator. In this article, we’ll walk through all of them, show live code examples you can run immediately, and highlight 🪄 the most impactful piece inside the code.
Finalized Features
JEP 503: Remove the 32-bit x86 Port
# Java won’t work at all if you are on a 32-bit x86 OS
# This command will produce errors
java -version
The official removal of the 32-bit x86 architecture support from the OpenJDK HotSpot JVM:
JEP 506: Scoped Values
Scoped Values let you safely and efficiently share immutable data with child threads without using thread-locals:
void main() { ScopedValue<String> scopedValue = ScopedValue.newInstance(); // 🪄 ScopedValue usage ScopedValue.where(scopedValue, "Java 25!").run(() -> { // Output: `Hello, Java 25!` IO.println("Hello, " + scopedValue.get()); }); }
JEP 510: Key Derivation Function API
Finalizes a unified, standard Java API for secure key derivation (e.g., HKDF, Argon2), enabling crypto providers and protocols to safely generate new keys from existing key material:
void main() throws Exception { byte[] ikm = "supersecret".getBytes(StandardCharsets.UTF_8); byte[] salt = "salty".getBytes(StandardCharsets.UTF_8); byte[] info = "context".getBytes(StandardCharsets.UTF_8); int keyLen = 32; // 256-bit key KDF kdf = KDF.getInstance("HKDF-SHA256"); HKDFParameterSpec params = HKDFParameterSpec.ofExtract() .addIKM(ikm) .addSalt(salt) .thenExpand(info, keyLen); // 🪄 Derive the key SecretKey derived = kdf.deriveKey("AES", params); // Output: `Derived AES key (hex): 5852801d2f50087728...bdcbe268680b71fdbdb` IO.println("Derived AES key (hex): " + HexFormat.of().formatHex(derived.getEncoded())); }
JEP 511: Module Import Declarations
Lets you import entire modules directly in code, reducing repetitive import statements:
/** // Instead of this: // import com.example.mymodule.api.SomeClass; // import com.example.mymodule.api.AnotherClass; // import com.example.mymodule.util.Helper; import module com.example.mymodule; */ // Instead of: // import java.util.List; // import java.util.Map; // import java.nio.file.Path; // import java.time.LocalDate; // One single module import: import module java.base; void main() { // 🪄 No explicit imports needed for core types like List, Path, or Map List<String> list = List.of("a", "b"); Map<String, Integer> map = Map.of("x", 1); Path path = Path.of("test.txt"); LocalDate now = LocalDate.now(); }
JEP 512: Compact Source Files and Instance Main Methods
Simplifies small programs by allowing class-less source files and instance main methods:
void main() { // 🪄 List auto-imported var nums = List.of(1, 2, 3, 4); // IO available without import // Output: `Numbers: [1, 2, 3, 4]` IO.println("Numbers: " + nums); }
You will notice that all code samples in this article are following the new JEP 512 coding pattern.
JEP 513: Flexible Constructor Bodies
Removes the restriction that super(..) or this(..) must be the first statement in a constructor:
void main() { // Output: `My name is: Java 25` new Employee("Java 25"); // Throws exception: `Name must not be blank` new Employee(""); } class Person { final String name; public Person(String name) { this.name = name; IO.println("My name is: " + this.name); } } class Employee extends Person { public Employee(String name) { if (name == null || name.isBlank()) { throw new IllegalArgumentException("Name must not be blank"); } // 🪄 super() allowed after validation super(name.trim()); } }
JEP 514: Ahead-of-Time Command-Line Ergonomics
Introduces a new -XX:AOTCacheOutput=.. JVM option that merges the training run and AOT cache creation into a single, streamlined command:
# 🪄 One command instead of two: java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App # More concrete demo in JEP 515
JEP 515: Ahead-of-Time Method Profiling
Speeds up Java application warm-up by embedding method execution profiles from a training run into the AOT cache, enabling immediate JIT optimization on startup without waiting for runtime profiling:
// Demo code that takes some time to execute void main() { long start = System.nanoTime(); // Simulate a "hot method" that gets called often for (int i = 0; i < 1_000_000; i++) { fibonacci(20); } long end = System.nanoTime(); IO.println("Execution time: " + (end - start) / 1_000_000 + " ms"); } // A small CPU-heavy method to make profiling visible static int fibonacci(int n) { if (n <= 1) return n; return fibonacci(n - 1) + fibonacci(n - 2); }
# Running this will take some time to finish execution java --source 25 Example.java # Output `Execution time: 39401 ms`
# Incorporating the shorter command line from JEP 514 # And generate an AOT cache java -XX:AOTCacheOutput=hotdemo.aot --source 25 Example.java # Execution time will still take some time to finish, but this is AOT # Output: ``` Execution time: 39463 ms Temporary AOTConfiguration recorded: hotdemo.aot.config Launching child process java to assemble AOT cache hotdemo.aot using configuration hotdemo.aot.config Picked up JAVA_TOOL_OPTIONS: -Djdk.internal.javac.source=25 --add-modules=ALL-DEFAULT -XX:AOTCacheOutput=hotdemo.aot -XX:AOTConfiguration=hotdemo.aot.config -XX:AOTMode=create Reading AOTConfiguration hotdemo.aot.config and writing AOTCache hotdemo.aot AOTCache creation is complete: hotdemo.aot 25948160 bytes Removed temporary AOT configuration file hotdemo.aot.config ``` # 🪄 Now we use the cache in subsequent runs java -XX:AOTCache=hotdemo.aot --source 25 Example.java # Output (varies based on platform) `Execution time: 26449 ms`
JEP 518: JFR Cooperative Sampling
Enhances Java Flight Recorder’s stability by delaying stack-walking to safe, well-defined “safepoints” via cooperative sampling—avoiding risky heuristics and improving reliability:
# Let’s reuse a simple code, like the one used in JEP 512, to observe this # Start JFR recording with SafepointLatency events. java -XX:StartFlightRecording=filename=cooperative.jfr,\ jdk.SafepointLatency#enabled=true --source 25 Example.java # 🪄 Inspect the results jfr print --events jdk.SafepointLatency cooperative.jfr # Output: ``` dk.SafepointLatency { startTime = ... duration = 0.0315 ms threadState = "_thread_in_Java" eventThread = "main" (javaThreadId = 3) stackTrace = [ jdk.internal.classfile.impl.DirectCodeBuilder.localAccess(int, int) line: 508 jdk.internal.classfile.impl.DirectCodeBuilder.astore(int) line: 1029 jdk.internal.classfile.impl.DirectCodeBuilder.storeLocal(TypeKind, int) line: 901 ... ] } jdk.SafepointLatency { startTime = ... duration = 0.0459 ms threadState = "_thread_in_Java" eventThread = "main" (javaThreadId = 3) stackTrace = [ java.util.ImmutableCollections$SetN.probe(Object) line: 1253 java.util.ImmutableCollections$SetN.<init>(Object[]) line: 1162 java.util.Set.of(Object[]) line: 706 ... ] } ... ```
JEP 519: Compact Object Headers
Promotes compact object headers—initially experimental—to a fully supported product feature, reducing object header size and optimizing memory efficiency and performance:
// Sample code that allocates a big number of Objects into the memory void main() throws Exception { Runtime rt = Runtime.getRuntime(); long before = rt.totalMemory() - rt.freeMemory(); // Allocate many objects int count = 10_000_000; Object[] arr = new Object[count]; for (int i = 0; i < count; i++) { arr[i] = new Object(); } long after = rt.totalMemory() - rt.freeMemory(); IO.println("Memory used: " + ((after - before) / (1024 * 1024)) + " MB"); }
# Running the code would output: `Memory used: 193 MB` java --source 25 Example.java
# 🪄 Enabling compact header `-XX:+UseCompactObjectHeaders` # Output: `Memory used: 116 MB` # Saves ~77 MB in this simple example. java -XX:+UseCompactObjectHeaders --source 25 Example.java
JEP 520: JFR Method Timing & Tracing
Enhances Java Flight Recorder by allowing precise method-level timing and tracing through bytecode instrumentation—without modifying source code. You can filter by method name, class, or annotation:
// Starting with a simple code with heavy method invocations void main() { // Run some methods repeatedly so JFR has data to capture for (int i = 0; i < 5_000; i++) { doWork(); } } void doWork() { double sum = 0; for (int i = 0; i < 1000; i++) { sum += Math.sqrt(i); } }
# 🪄 Check the `doWork` method timing java -XX:StartFlightRecording:filename=timing.jfr,\ jdk.MethodTiming#filter=Example::doWork --source 25 Example.java jfr view method-timing timing.jfr # Output: ``` [0.727s][info][jfr,startup] Started recording 1. No limit specified, using maxsize=250MB as default. [0.727s][info][jfr,startup] [0.727s][info][jfr,startup] Use jcmd 6911 JFR.dump name=1 to copy recording data to file. Method Timing Timed Method Invocations Minimum Time Average Time Maximum Time ---------------------------- ----------- ------------ ------------ ------------ Example.doWork() 5,000 0.000008 ms 0.001540 ms 0.097200 ms
# Method tracing can also be enabled (produces lengthy output, discussed in JEP 518) java -XX:StartFlightRecording:filename=demo.jfr,\ jdk.MethodTrace#filter=Example::doWork --source 25 Example.java
JEP 521: Generational Shenandoah
Introduces a significant enhancement to the Shenandoah garbage collector by promoting its generational mode from an experimental feature to a fully supported product feature:
// Simple code to observe the Shenandoah GC effect static class BigObject { int[] data = new int[10_000]; // ~40 KB } void main() throws Exception { List<BigObject> list = new ArrayList<>(); for (int i = 0; i < 50_000; i++) { list.add(new BigObject()); if (i % 1000 == 0) { Thread.sleep(50); // slow down allocation to observe GC } } Thread.sleep(10_000); // Keep app alive to observe GC logs }
# 🪄 Run With Generational Shenandoah java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational \ -Xlog:gc* --source 25 Example.java # Output: ``` [0.123s][info][gc,start ] GC(0) Pause Young (G1 Evacuation) [0.124s][info][gc,task ] GC(0) Using 4 workers [0.127s][info][gc,phases ] GC(0) PreEvacuate Young [0.128s][info][gc,phases ] GC(0) Evacuate Young [0.129s][info][gc,phases ] GC(0) PostEvacuate Young [0.130s][info][gc,heap ] GC(0) Heap before GC: 200 MB [0.131s][info][gc,heap ] GC(0) Heap after GC: 120 MB [0.132s][info][gc,metaspace ] GC(0) Metaspace used: 10 MB [0.133s][info][gc,stats ] GC(0) Pause Young: 3.5 ms ... ``` # Young collections are very fast (a few ms), cleaning up short-lived objects. # Mixed/Old collections are slightly longer but still low-latency. # Heap usage decreases gradually, # as generational GC separates short-lived objects from long-lived ones. # Pause times are consistently low because Shenandoah does concurrent evacuation.
Preview Features
To run any of the provided sample code in this section, you must enable preview, e.g.
java --enable-preview --source 25 Example.java
JEP 470: PEM Encodings of Cryptographic Objects
Provides a built-in API to encode/decode cryptographic keys and certificates in PEM format, removing the need for manual parsing or external libraries:
void main() { String pemText = """ -----BEGIN PUBLIC KEY----- MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAoO4f2xOp7o0dSnjf3IcF A/bVo1gz84YjQWflO+bXzPf2mWQWka3wYJGgP6OZGCIPffUkEG+0Ztf/2g4cWRE1 Uop+eU3eyBTbtUkywWxNY/vLHjJ2voEGhn5kuI5oZCAOuaESJd0s6TkNErfg04XZ G2J+e6EtB0arpgPXIf5iSTjW1ccDEG+5/7YJShKPZY0+MDyxSxjApzdnICktDg8o 4hYKYstb3o+2SPovoFhfBlMIFXL9mfUYDOQjFHG8fCnDxMfFUOBXqRwz/eYpai9w mhjGoeQX9FrMliKkabgTt8hEDiKaddkSVs9sVcx7mTvSeFyzzziaKgKVz60smE4S MTDC5aOuqoPkyWEc5CPgoFR/SZkLYmWcwLXv7RMZCwckJmq+P281H7C8aKx6AX0R ytJFQAfRGavdBTn5lOUTCVrIM7Rb81tyJA9+gQpWF6GI3cqjMa5XBHbit/y0MffO GvWlXx6Gi/vBJFegbDK/tsTxnPcXHVi/OTGpe2hauI7jAgMBAAE= -----END PUBLIC KEY----- """; // 🪄 Decode the key PublicKey pk = PEMDecoder.of().decode(pemText, PublicKey.class); // Output: `Algorithm: RSA` IO.println("Algorithm: " + pk.getAlgorithm()); }
JEP 502: Stable Values
Introduces single-assignment, immutable containers for values that are initialized once and safely shared across threads:
void main() { StableValue<String> value = StableValue.of(); // 🪄 Output means that exception has been thrown. // Output: `Hello, Java 25!` try { value.setOrThrow("Hello, Java 25!"); value.setOrThrow("❌ Can only be set once."); } catch (IllegalStateException e) { IO.println(value); } }
JEP 505: Structured Concurrency
Adds an API to manage multiple tasks as a single unit of work, simplifying error handling and cancellation in concurrent programming:
void main() throws Exception { var scope = StructuredTaskScope.open(); var f2 = scope.fork(() -> "Java 25"); var f1 = scope.fork(() -> "Hello"); // 🪄 Waits for all subtasks to complete or fail scope.join(); // Output: `Hello, Java 25!` IO.println(f1.get() + ", " + f2.get() + "!"); }
JEP 507: Primitive Types in Patterns, instanceof, and switch
Expands pattern matching so that instanceof and switch can match primitive types—like int, long, or double—making pattern logic more expressive and uniform across all data types:
void main() { Object o = 42; // 🪄 instance of primative // Output: `It's an int: 42` if (o instanceof int i) { IO.println("It's an int: " + i); } o = 42d; // 🪄 switch on primative // Output: `Switched on double: 42.0` switch (o) { case int i -> IO.println("Switched on int: " + i); case double i -> IO.println("Switched on double: " + i); default -> IO.println( "Switched on " + o.getClass().getName() + ": " + o); } }
Experimental Features
JEP 509: JFR CPU-Time Profiling
Introduces Linux-only, CPU-time-based profiling to JDK Flight Recorder, capturing precise per-thread CPU usage—including time spent in native code—more accurately than traditional wall-clock sampling:
// To test this out, first create a simple, CPU-bound calculation // to generate a profileable workload, in a file let say CPUProfiling.java void main() { IO.println("Starting CPU-intensive task..."); for (long i = 0; i < 2_000_000_000L; i++) { Math.sqrt(Math.log(i + 1)); } IO.println("Task complete."); }
# Compile the code javac --enable-preview --release 25 CPUProfiling.java
# 🪄 Run the program with CPU profiler (on Linux) java -XX:StartFlightRecording=jdk.CPUTimeSample#enabled=true,\ jdk.CPUTimeSample#throttle=500/s,filename=cpu.jfr CPUProfiling # Examine the content of cpu.jfr (or visualize with JDK Mission Control) jfr view cpu-time-hot-methods cpu.jfr # Sample output: ``` Method Samples Percent --------------------------------------------------------------- ------- ------- CPUProfiling.main(String[]) 2,711 99.93% java.util.Properties$LineReader.readLine() 1 0.04% jdk.internal.classfile.impl.Util.buildingCode(Consumer) 1 0.04% ```
Incubator Features
To run the code in this section, you must enable preview and add related incubator module, e.g.
java --enable-preview --source 25 --add-modules jdk.incubator.vector Example.java
JEP 508: Vector API
Adds a new set of APIs to express vector computations that compile at runtime into optimal hardware instructions on supported CPU architectures:
import jdk.incubator.vector.*; void main() { float[] a = {1f, 2f, 3f, 4f}; float[] b = {5f, 6f, 7f, 8f}; // A species defines the vector shape (bit-width & element type). // SPECIES_128 means 128-bit wide vectors, i.e. can hold 4 floats. // On supported CPUs, this maps to SIMD registers // e.g. XMM registers on x86, NEON on ARM, ..etc. var species = FloatVector.SPECIES_128; // Wraps the arrays into vector objects var va = FloatVector.fromArray(species, a, 0); var vb = FloatVector.fromArray(species, b, 0); // 🪄 Vector addition in parallel // [1,2,3,4] + [5,6,7,8] → [6,8,10,12] // On supported hardware, it's a single CPU instruction rather than a loop. var vc = va.add(vb); // Moves the SIMD register contents back into a normal Java array. float[] result = new float[species.length()]; vc.intoArray(result, 0); // Output: `Vector sum: [6.0, 8.0, 10.0, 12.0]` IO.println("Vector sum: " + java.util.Arrays.toString(result)); }

This article is part of the magazine issue ’Java 25 – Part 1′.
You can read the complete issue with all contributions here.