Java 25 ZGC Default — CPU Spike from Concurrent Marking
CPU usage spiked from 40% to 55% after Java 25's ZGC default.
- Java 25 is the latest LTS release, supported until 2030 — the upgrade path for enterprises and projects like Minecraft.
- ZGC becomes the default garbage collector, targeting sub-1ms pause times with zero config changes.
- Compact Object Headers (JEP 450) reduce per-object memory overhead by up to 50% on 64-bit systems.
- Pattern Matching for switch and Record Patterns exit preview — stable, no --enable-preview flag needed.
- Virtual Thread edge cases fixed — production-safe for IO-bound workloads.
- Migration from Java 21 is low-friction with no major breaking changes.
A few weeks ago, Mojang announced Minecraft Java Edition 26.1 requires Java 25. Developers asked: why this version, and what's in it?
Turns out, quite a lot. Java 25 is an LTS release — same category as Java 11, 17, and 21. Non-LTS releases (22, 23, 24) are fine for experimentation, but nobody runs production workloads on them.
This article covers what Java 25 actually changes. Honest takes on what matters and what's mostly marketing.
First — What Is an LTS Release and Why Does It Matter?
Java ships a new version every 6 months. Most of these are short-term releases — supported for 6 months and then dropped. LTS (Long-Term Support) releases are different — they get security patches and bug fixes for years.
- Oracle and major vendors support it until at least 2030
- Cloud providers like AWS and Google Cloud optimise their runtimes for it
- Frameworks like Spring Boot 3.3+ and Quarkus fully certify against it
- Large organisations actually upgrade to it
This is why Minecraft waited. They weren't going to require Java 22 and then have to require Java 25 a year later. They skipped straight to the LTS.
For your own projects, the practical takeaway is simple: if you're on Java 21, start planning an upgrade. Java 17's LTS support ends in 2026.
Complete JEP Reference Table for Java 25
Here is the full list of Java Enhancement Proposals (JEPs) that shipped in JDK 25. Use this as a quick reference to understand what each change delivers.
| JEP | Title | Status | Description |
|---|---|---|---|
| 450 | Compact Object Headers | Final | Reduce per-object metadata from 96-128 bits to 64 bits on 64-bit JVMs, cutting heap usage by 10-20%. |
| 472 | Prepare to Restrict the Use of JNI | Final | Warn when JNI is used; stronger encapsulation planned for future releases. |
| 474 | ZGC: Generational Mode by Default | Final | Make generational ZGC the default, improving memory efficiency. |
| 478 | Key Derivation Function API | Preview | Standard API for KDFs (e.g., HKDF) to derive cryptographic keys. |
| 479 | Remove the Port of the BSD Kernel (x86-32) | Final | Drop support for 32-bit BSD, reducing maintenance burden. |
| 480 | Structured Concurrency | Third Preview | Incubate structured concurrency with improved API based on feedback. |
| 481 | Scoped Values | Third Preview | Refine scoped values API for sharing immutable data across threads. |
| 482 | Flexible Constructor Bodies | Second Preview | Allow in constructors to appear after other statements. |
| 483 | Ahead-of-Time Class Linking and Optimization | Final | Improve startup time by linking classes at build time. |
| 484 | Class-File API | Final | Standard API for parsing, generating, and transforming class files. |
| 485 | Stream Gatherers | Preview | Extend streams with custom intermediate operations. |
| 486 | Permanently Disable the Security Manager | Final | Remove the Security Manager APIs (deprecated since Java 17). |
This table is the authoritative source; each JEP links to the specification page on OpenJDK. For migration, focus on the Final JEPs — Preview features are unstable and subject to change.
LTS Feature Progression: Java 17 → Java 21 → Java 25
Understanding the feature progression across the last three LTS releases helps you plan your upgrade journey. Here's a side-by-side comparison of the most impactful language and JVM changes.
| Feature | Java 17 LTS (2021) | Java 21 LTS (2023) | Java 25 LTS (2026) |
|---|---|---|---|
| Default GC | G1GC | G1GC | ZGC (generational) |
| Pattern Matching for switch | Preview | Preview (2nd) | Final |
| Record Patterns | — | Preview | Final |
| Virtual Threads | Incubator | Final | Stable (edge cases fixed) |
| Sealed Classes | Final | Final | Final |
| Compact Object Headers | — | — | Final (JEP 450) |
| Records | Final | Final | Final |
| Text Blocks | Final | Final | Final |
| Foreign Function & Memory API | Incubator | Final | Final |
| Structured Concurrency | Incubator | Preview | Preview (3rd) |
| Scoped Values | Incubator | Preview | Preview (3rd) |
| Class-File API | — | — | Final |
| AOT Class Linking | — | — | Final |
| Security Manager | Deprecated | Deprecated | Removed (final) |
removal | Deprecated | Removed | Removed |
Key trend: each LTS release doubles down on concurrency and memory efficiency. Java 25 is the first where the default GC is latency-optimised, and object overhead is automatically reduced.
Removals and Deprecations in Java 25
Every Java release removes or deprecates old APIs. Java 25 is no exception. Here is the definitive list of what you must account for when migrating.
Fully removed in Java 25: - finalize() method — removed. Use Cleaner or AutoCloseable instead. The @Deprecated(since="9", forRemoval=true) tag finally acted on. - Security Manager — permanently disabled (JEP 486). Calling System.setSecurityManager() throws UnsupportedOperationException. - Port of the BSD kernel (x86-32) — removed (JEP 479). No more 32-bit macOS/BSD support. - Legacy File-based URL constructors — removed. Use URI.toURL(). - java.rmi activation framework — removed (deprecated in Java 15).
Deprecated in Java 25 (likely removed in future): - -XX:+UseAdaptiveSizePolicy (G1GC only) — no effect; to be removed. - -Xlog:gc=info format may change; migrate to structured logging via -Xlog:gc:file=.... - ThreadGroup stop/resume/suspend — deprecated for removal. - java.security.Policy — deprecated, use modular security.
Module system changes: - java.se.ee module removed (already removed from JDK 9+? Actually java.se.ee was removed in 11. For 25, no further removal). - But note: The java.corba module was removed in 11; java.xml.ws in 11. In 25, no additional module removals. - The jdk.unsupported module still exists for internal APIs (e.g., sun.misc.Unsafe) but use is strongly discouraged. - JNI restrictions are warned (JEP 472); future releases may enforce encapsulation.
Run jdeprscan on your codebase to identify deprecated API usage before upgrading.
finalize() or uses the Security Manager. Both are now dead code — compile and test will fail if you haven't migrated. Also, any JNI-heavy libraries may trigger warnings in Java 25 logs. Monitor them; plan to replace them before the next LTS.Java 21 to Java 25 Migration Guide
Migrating from Java 21 to Java 25 is intentionally low-friction, but there are areas that need attention. Follow this guide step by step.
1. Check build tool compatibility - Maven: use maven-compiler-plugin:3.13.0+ (supports Java 25). - Gradle: upgrade to 8.10+ (supports Java 25 toolchain).
2. Remove preview flags - Remove --enable-preview from javac flags if you were using Pattern Matching for switch or Record Patterns in preview. They are now final. - Remove -XX:+UseZGC if present (it's default).
3. Handle removed APIs - Replace with finalize()Cleaner (example below). - Replace System.setSecurityManager(...) with a custom access control or remove entirely. - Check for sun.* internal API usage: jdeprscan --for-removal --class-path ....
4. Test GC behaviour - Run with ZGC default. If CPU usage is >20% higher, tune -XX:ConcGCThreads or revert to G1GC. - Verify compact object headers work: java -d64 -version (64-bit required). - Check that heap metrics improve: jcmd <pid> GC.heap_info.
5. Update module-info if needed - No new module restrictions in Java 25, but if you used --add-exports for sun.misc.Unsafe, those still work (deprecated). - Consider migrating to java.lang.foreign (MemorySegment) for off-heap operations.
6. Validate virtual thread workloads - Ensure no pinned threads due to synchronized blocks; replace with ReentrantLock. - Avoid large ThreadLocal data; use Scoped Values (preview) if appropriate.
7. Run a full regression test - Compile and run tests with Java 25. Use --release 25 in javac. - Deploy to a staging environment and monitor CPU, memory, and latency for 48 hours.
Breaking changes (rare but real): - Custom sun.misc.Cleaner implementations may break due to internal changes. - java.lang.Compiler is removed (already in Java 9? Actually removed in Java 9, but check). - javax.security.auth.Policy is removed (replaced by sun.security.provider.PolicyFile? note only deprecated).
Sample Cleaner migration: ```java // Before (removed) @Override protected void finalize() { cleanup(); }
// After private final Cleaner cleaner; private final Cleaner.Cleanable cleanable;
public MyResource() { cleaner = Cleaner.create(); cleanable = cleaner.register(this, () -> cleanup()); } ```
--enable-preview blindly. Some preview features may have subtle API changes between preview and final. Always recompile and test.ZGC Is Now the Default GC — This One Actually Matters
This is the change most developers will feel without changing a single line of code. Up until Java 25, the default garbage collector was G1GC. G1 is good — but it has stop-the-world pauses. For most apps this is fine. For latency-sensitive apps (game servers, trading systems, real-time APIs), those pauses are a problem.
ZGC keeps pauses under 1 millisecond regardless of heap size. It does this by doing most of its work concurrently — while your application is still running. Minecraft's chunk loading used to cause noticeable lag spikes because G1GC would kick in during heavy world generation. ZGC smooths that out.
The best part: you don't have to do anything. No JVM flags, no config changes. If you were already using -XX:+UseZGC, you can remove that flag.
ZGC vs G1GC vs Shenandoah: Performance Comparison
With Java 25 defaulting to ZGC, you have three modern GCs to choose from. Here's a direct comparison to help you decide for your workload.
| Aspect | ZGC (Generational) | G1GC | Shenandoah |
|---|---|---|---|
| Pause time target | <1ms | 10-100ms | <10ms |
| Pause time model | Mostly concurrent; small stop-the-world phases | Concurrent marking; stop-the-world for compaction | Concurrent compaction; very short pauses |
| Heap size sweet spot | 4GB – 1TB+ (pauses independent) | 4GB – 64GB (pauses grow with heap) | 4GB – 512GB (pauses grow slowly) |
| CPU cost | ~10-20% higher than G1GC (concurrent threads) | Baseline (lower) | ~15-30% higher than G1GC (more concurrent work) |
| Throughput | Slightly lower (concurrent overhead) | High (stop-world pauses are idle CPU) | Lower than G1GC but better than ZGC in some benchmarks |
| Generational support | Yes (default in 25) | Yes (region-based) | No (full heap compaction) |
| Memory overhead | ~2-3% of heap (object pointer tables) | ~1-2% of heap (region tracking) | ~3-5% of heap (load barriers) |
| Diagnosability | Good (detailed GC logs) | Excellent (mature tools) | Good (similar logging to ZGC) |
| Production readiness | Excellent (default in 25) | Excellent (battle-tested) | Good (used in low-pause environments) |
When to choose which: - ZGC: Latency-critical apps, large heaps, unpredictable allocation patterns. - G1GC: Throughput-oriented batch jobs, moderate-heap apps, CPU-limited environments. - Shenandoah: Low-pause requirement but willing to trade more CPU than ZGC; often used in large-scale web servers.
Shenandoah is not the default in any OpenJDK build (Oracle, Temurin). It must be enabled explicitly: -XX:+UseShenandoahGC. It is available in Amazon Corretto and Red Hat builds.
-XX:ConcGCThreads and -XX:ZAllocationSpikeTolerance before considering reverting to G1GC. Shenandoah is rarely the answer unless you need pauses under 10ms and can spare 30% more CPU.Compact Object Headers — Less RAM, Same Code
Every Java object carries a header — JVM bookkeeping data. In Java 21 and earlier, this is typically 96 to 128 bits. Java 25 ships compact object headers (JEP 450) as a stable feature, cutting that down to 64 bits.
Now 32 bits doesn't sound like a lot. But a typical backend application might have tens of millions of live objects. Across that many objects, you're looking at a meaningful drop in heap usage — less GC pressure, better cache locality, and lower cloud bills.
Pattern Matching for switch — Finally Out of Preview
This one has been in preview since Java 17. Four release cycles later, it's fully finalised in Java 25 — which means it's stable, won't change, and you can use it without --enable-preview.
If you've written a lot of instanceof chains, you'll like this.
Record Patterns — Destructure in One Line
Also finalised in Java 25. Record patterns let you unpack record components directly inside a pattern match — no separate accessor calls needed.
Virtual Threads Are Stable — Use Them
Virtual threads were finalised in Java 21, but Java 25 fixes a bunch of edge cases that made people hesitant to use them in production — particularly around thread-local variables and synchronisation with native code.
If you're running a web server and still using a fixed thread pool, switch to virtual threads. The code change is one line.
Executors.newVirtualThreadPerTaskExecutor().So Why Did Minecraft Really Upgrade?
Three reasons, honestly:
ZGC by default — chunk loading in large worlds triggered G1GC pauses that players felt as lag. ZGC's sub-millisecond pauses remove that.
Compact object headers — Minecraft tracks millions of blocks, entities, and chunks simultaneously. Cutting header size reduces baseline memory pressure, which is why Mojang could confidently bump the default launcher RAM to 4GB without it feeling wasteful.
It's LTS — Mojang isn't going to build on a release that's out of support in 6 months. Java 25 takes them through to 2030. Same reason they were on Java 21 before this.
There's also a fourth, less talked-about reason: Java 25 is the first version to fully unobfuscate the Minecraft codebase under the new release process. But that's more of an internal tooling story than a Java feature story.
How to Actually Upgrade
The migration from Java 21 to Java 25 is low-friction for most projects. There are no major breaking API changes.
ZGC Default Causes Unexpected CPU Spike in Production
- Test GC behavior under production load before rolling out a new default GC.
- Monitor CPU and latency metrics during JVM upgrades, not just memory.
- Understand that ZGC trades throughput for latency — it's not free; budget for the CPU impact.
Key takeaways
Common mistakes to avoid
4 patternsManually enabling ZGC with -XX:+UseZGC
java -XX:+PrintFlagsFinal -version | grep UseZGC.Assuming Compact Object Headers work on 32-bit JVMs
java -d64 -version. Use 64-bit JVM for compact headers.Using virtual threads for CPU-bound tasks
Keeping --enable-preview flag after upgrade
Interview Questions on This Topic
Explain the difference between ZGC and G1GC. Why would a low-latency application prefer ZGC in Java 25?
Frequently Asked Questions
That's Java 8+ Features. Mark it forged?
10 min read · try the examples if you haven't