How Java 25 Quietly Changed Everything You Thought You Knew About Performance
“The JVM is not a virtual machine. It’s a time machine - taking code written yesterday and making it run faster tomorrow, without you touching a single line.”
Prologue: The City Beneath Your Code
Imagine your Java application as a bustling metropolis. The Heap is the residential district - sprawling, crowded, constantly being cleaned by sanitation trucks (the Garbage Collector). The Stack is the business district - efficient, vertical, each building demolished the moment its purpose is served. And somewhere in between, unseen engineers work day and night to make everything run smoother, faster, cheaper.
For thirty years, this city has been evolving. While Oracle announced Java 25 as the latest Long-Term Support release with the usual fanfare, the quiet elegance lies in how these improvements arrive: no code changes, no migration pain, just add a flag and watch performance metrics improve. The revolution isn’t in the announcement-it’s in the effortless adoption. In September 2025, Java 25 arrived carrying a feature that would make CTOs do double-takes at their monitoring dashboards: up to 30% CPU reduction, zero code changes required.
How? Let’s open the hood.
Part I: Anatomy - What Actually Happens When You Run java -jar
The Assembly Line
Your Java code doesn’t “run.” It transforms:
YourCode.java
↓
javac
↓
Bytecode (.class)
↓
Class Loader
↓
Bytecode Verifier
↓
Interpreter (slow, immediate)
↓
C1 Compiler (warm, quick optimizations)
↓
Profiling (learning phase)
↓
C2 Compiler (hot, aggressive optimizations)
↓
Native Machine Code (fast)
This is tiered compilation, and it’s been Java’s secret sauce since version 8. The JVM watches how your code behaves - which methods get called frequently, which branches are taken, which loops matter. Then it recompiles the hot paths into highly optimized native code.
But here’s the thing: for all its sophistication, the JVM has been carrying historical baggage. Every object in Java carries a header - metadata about the object itself. And for thirty years, that header has been… chunky.
Part II: Beyond the Headlines - Incremental Improvements
While Compact Object Headers steal the spotlight, Java 25 continues the C2 compiler’s ongoing refinement. These aren’t revolutionary changes, but they add up:
| Improvement | What It Does | Typical Speedup |
|---|---|---|
| Merge Store Optimization | Better handling of sequential byte stores, including reverse-order operations | 2-4x for network byte-order conversions |
| Math.max/min Recognition | Improved auto-vectorization of common math operations | 1.5-2x on numerical workloads |
| Pattern Recognition | More loops eligible for SIMD auto-vectorization | Varies by workload |
// This pattern benefits from merge store optimization:
byte[] networkBuffer = new byte[1024];
for (int i = 0; i < data.length; i++) {
networkBuffer[i] = (byte) ((data[i] >> 8) & 0xFF); // Big-endian conversion
}
These improvements demonstrate the JVM’s continuous evolution: your unchanged code gets faster because the compiler keeps getting smarter, release by release.
Part III: The 12-Byte Tax
Meet Your Object’s Overhead
Create a simple object in Java:
class Point {
int x;
int y;
}
How much memory does it consume? If you said “8 bytes for two ints,” you’d be wrong by 60%.
In the traditional HotSpot JVM, every object carries:
- Mark Word (64 bits): GC age, identity hash code, lock information
- Class Pointer (32 bits compressed): Reference to class metadata
- Padding (32 bits): Alignment to 8-byte boundary
Total: 12 bytes of header + 8 bytes of data = 20 bytes.
For an object with just two integers, 40% of the memory is overhead.
This isn’t just academic. In microservices architectures, in-memory caches, and data processing pipelines, applications create millions of small objects per second. That 12-byte tax compounds into gigabytes of wasted RAM and countless CPU cycles spent managing the bloat.
Amazon measured this in production on x86_64 Linux with G1GC: some services spent up to 20% of their heap just on object headers, particularly in microservices with high object allocation rates.
Something had to change.
When benefits are minimal: Applications with large object-to-header ratios (CAD tools, video processing, applications using mostly Direct ByteBuffers) won’t see dramatic improvements from Compact Object Headers-the header overhead was already negligible compared to the data payload.
Part IV: The Compression That Changed Everything
JEP 519: Compact Object Headers
Java 25 introduces Compact Object Headers (JEP 519), and the math is elegant in its simplicity:
| Component | Traditional | Compact |
|---|---|---|
| Mark Word | 64 bits | 64 bits (shared) |
| Class Pointer | 32 bits | 22 bits (compressed) |
| Total | 96 bits (12 bytes) | 64 bits (8 bytes) |
The trick? The JVM engineers realized they could overlay the class pointer onto unused bits in the mark word. Most objects don’t need all 64 bits of metadata simultaneously. By carefully encoding the class pointer into just 22 bits - enough to address 4 million unique classes - they freed up 4 bytes per object.
33% header reduction. Up to 30% CPU savings in production.
Amazon’s early adopters reported this across hundreds of services running on x86_64 Linux, primarily microservices with high object allocation rates:
- 22% less heap usage (SPECjbb2015 benchmark on x86_64)
- Up to 30% CPU reduction (production workloads using G1GC)
- 15% fewer GC cycles (G1 and Parallel collectors)
- 10% faster JSON parsing (string-heavy workloads)
Hardware context: Most benchmarks and production results cited are from x86_64 deployments. ARM64 (Graviton) results may vary. Applications spending most time in I/O operations see proportionally smaller CPU improvements since the gains come from reduced memory management overhead.
When Compact Object Headers Shine
The 30% figure makes headlines, but context matters. Compact Object Headers provide the most benefit for:
- Microservices architectures - Spring Boot creates thousands of small objects per request
- In-memory caches - Caffeine, Ehcache, custom caches with small entries
- Data processing pipelines - JSON parsing, message deserialization
- Applications with high object churn - frequent allocation of short-lived objects
Conversely, applications with large object-to-header ratios (e.g., processing massive byte arrays or image buffers) won’t see dramatic improvements-the header overhead was already negligible compared to the data.
The Magic Flag
Enabling this revolution requires exactly one JVM argument:
# Java 24 (experimental)
java -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders MyApp
# Java 25 (production-ready)
java -XX:+UseCompactObjectHeaders MyApp
That’s it. No code changes. No recompilation. No regression testing. Just add the flag and watch your metrics.
What to monitor:
# Before and after comparison with JFR
java -XX:+UseCompactObjectHeaders -XX:StartFlightRecording=duration=60s,filename=compact.jfr -jar app.jar
java -XX:StartFlightRecording=duration=60s,filename=baseline.jfr -jar app.jar
# Compare heap usage
target:~$ jfr print compact.jfr | grep -A5 "Heap Summary"
Part V: The Warmup Problem - Solved
AOT Profiling and Project Leyden
Compact Headers solve memory efficiency. But Java 25 has another trick for speed.
Every JVM developer knows the warmup problem. When your application starts, it runs interpreted bytecode - slow. It takes time for the JIT compilers to identify hot paths and optimize them. In microservices environments where instances start and stop constantly, you’re always in warmup mode.
Java 25 introduces AOT (Ahead-of-Time) Profiling, part of Project Leyden:
- Training Run: Start your application with a special flag. Let it exercise real workloads.
- Profile Capture: The JVM saves optimization data - which methods matter, which branches are hot, which types are common.
- Production Deployment: Subsequent starts load the profile and begin optimized compilation immediately.
Result: 20-40% faster startup in production-ready Java 25, with early access builds of Project Leyden showing potential for even greater gains (approaching 70% in optimal scenarios) as the technology matures.
It’s worth noting that JEP 514 and JEP 515 in Java 25 represent the production-ready foundation of Project Leyden, but not its full vision. The most dramatic improvements will come as the project continues to evolve. Still, Quarkus framework benchmarks show measurable improvements in both startup times and warmup efficiency even with Java 25’s current capabilities.
AOT vs. CDS: Project Leyden builds upon Java’s existing Class Data Sharing (CDS) mechanism, which has been available since Java 5. While CDS focused on sharing class metadata between JVMs, Project Leyden’s AOT capabilities go further by capturing method profiles, resolving constant pool entries, and even pre-compiling hot methods. Think of CDS as the foundation and Leyden as the next evolution.
# Step 1: Training run - capture the profile
java -XX:AOTClassLoading=preserve -XX:AOTConfiguration=app.aotconf -jar app.jar
# Step 2: Production runs use the captured profile
java -XX:AOTClassLoading=preserve -XX:AOTConfiguration=app.aotconf -jar app.jar
For serverless and Kubernetes environments where cold starts directly impact user experience, this is transformative.
Part VI: Generational Shenandoah - The Other GC Gets an Upgrade
While ZGC grabbed headlines with its generational mode in Java 23, Java 25 brings the same treatment to Shenandoah GC (JEP 521).
Shenandoah has always been the low-pause alternative to G1, promising sub-millisecond pauses regardless of heap size. But like ZGC before it, Shenandoah treated all objects equally-missing optimization opportunities from generational hypothesis (most objects die young).
Generational Shenandoah changes the game:
- Separates young and old generations for more efficient collection
- Reduces pause times for workloads with typical generational behavior
- Improves throughput without sacrificing Shenandoah’s ultra-low latency promise
- Makes Shenandoah more competitive for mainstream applications
# Enable Generational Shenandoah
java -XX:+UseShenandoahGC -XX:+ShenandoahGCMode=generational -jar app.jar
ZGC vs. Shenandoah in Java 25: Both now offer generational modes. ZGC is optimized for massive heaps (up to 16TB), while Shenandoah targets a broader range of workloads. The choice depends on your specific latency and throughput requirements.
Part VII: Scoped Values - ThreadLocal’s Successor
Java 21 introduced Virtual Threads (millions of lightweight threads). Java 25 finalizes Scoped Values - a modern replacement for ThreadLocal that works seamlessly with virtual threads.
// OLD: ThreadLocal (expensive with millions of virtual threads)
private static final ThreadLocal<String> REQUEST_ID = new ThreadLocal<>();
void handleRequest(Request req) {
REQUEST_ID.set(req.getId()); // Duplicated per thread
try {
processRequest();
} finally {
REQUEST_ID.remove(); // Memory leak if forgotten!
}
}
// NEW: ScopedValue (efficient with virtual threads)
private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
void handleRequest(Request req) {
ScopedValue.where(REQUEST_ID, req.getId()).run(() -> {
// Automatically available to all callees
// Automatically cleaned up when scope exits
processRequest();
});
}
Unlike ThreadLocal, which duplicates state per thread (expensive with millions of virtual threads), Scoped Values allow shared access with less memory overhead and lower synchronization cost.
Migration Strategy
Scoped Values aren’t a drop-in replacement-you’ll need to refactor ThreadLocal code. But they can coexist during migration:
// Migration pattern: Both can coexist during transition
private static final ThreadLocal<String> OLD_REQUEST_ID = new ThreadLocal<>();
private static final ScopedValue<String> NEW_REQUEST_ID = ScopedValue.newInstance();
void handleRequest(Request req) {
// Set both during migration period
OLD_REQUEST_ID.set(req.getId());
ScopedValue.where(NEW_REQUEST_ID, req.getId()).run(() -> {
// New code uses NEW_REQUEST_ID
// Legacy code still uses OLD_REQUEST_ID
processRequest();
});
OLD_REQUEST_ID.remove(); // Still need cleanup for ThreadLocal!
}
Part VIII: What This Means for You
The Practical Checklist
If you’re on Java 21 or earlier:
- Upgrade to Java 25. The performance gains are real and require no code changes.
- Add
-XX:+UseCompactObjectHeadersto your JVM flags. - Monitor heap usage and GC frequency - you should see improvements immediately.
If you’re running microservices or serverless:
- Experiment with AOT profiling for faster cold starts.
- Measure startup time before and after - the difference might surprise you.
If you’re building high-throughput applications:
- Consider ZGC for sub-millisecond pauses. Generational ZGC (JEP 474) was finalized in Java 23 and is the default since then-by Java 25, it’s a well-tested, production-hardened feature.
- Alternatively, explore Generational Shenandoah (JEP 521), new in Java 25, which brings similar generational improvements to the Shenandoah GC for reduced pause times.
- Combine with Compact Headers for the ultimate memory-efficient setup.
Recommended Java 25 Configuration
java \
-XX:+UseCompactObjectHeaders \
-XX:+UseZGC \
-XX:MaxGCPauseMillis=100 \
-XX:+AlwaysPreTouch \
-Xms4g \
-Xmx4g \
-jar your-application.jar
Note: Generational ZGC (JEP 474) was finalized in Java 23 and is the default since then. By Java 25, it’s a mature, production-hardened feature. -XX:+ZGenerational is no longer required (but can be added for explicit clarity).
Part IX: When to Exercise Caution
Not All Improvements Fit All Workloads
While Java 25’s improvements are impressive, they’re not universal solutions. Here’s when to think twice:
Compact Object Headers:
- ❌ Don’t enable if you’re using native libraries that directly access object memory or assume fixed header layout
- ❌ ZGC support for Compact Object Headers on x64 is still being developed (check latest JDK updates)
- ❌ Cannot be combined with
-XX:-UseCompressedClassPointers(deprecated in Java 25 anyway) - ✅ Best for: applications with many small objects (microservices, caches, data pipelines, Spring Boot)
- ⚠️ Test thoroughly before production if you use JNI or custom native code
AOT Profiling (Project Leyden):
- ❌ Only beneficial if your application has predictable, repeatable startup patterns
- ❌ Highly dynamic applications that load different classes per execution won’t benefit as much
- ✅ Best for: microservices, serverless functions, containers with consistent workloads
Scoped Values vs ThreadLocal:
- ❌ Not a drop-in replacement-ThreadLocal code needs careful refactoring
- ❌ Designed for structured concurrency patterns
- ✅ Best for: new code using virtual threads and structured concurrency
Generational GC (ZGC & Shenandoah):
- ❌ May not benefit applications with atypical object lifetime patterns
- ✅ Best for: typical enterprise workloads with clear young/old generation separation
Epilogue: The Future is Already Here
We’ve reached an inflection point. For decades, Java performance improvements required developer effort - better algorithms, cleaner code, careful tuning. Java 25 flips the script: the platform itself has become the optimization engine.
Your code from 2010 runs faster today on Java 25 than it did when written. Not because you changed it, but because the JVM evolved underneath it.
This is the silent revolution. No press releases. No breaking changes. Just a flag (-XX:+UseCompactObjectHeaders) that unlocks 30% better resource efficiency. Just a runtime that learns from itself and optimizes without human intervention.
The JVM isn’t just a virtual machine anymore. It’s a self-improving system - one that carries the weight of thirty years of Java code and makes it lighter, faster, and more efficient with every release.
The future of Java performance isn’t about writing better code. It’s about running it on a better JVM.
That future arrived in September 2025, and by early 2026, production deployments are already showing the promised results. The silent revolution is well underway.
References
- JEP 519: Compact Object Headers
- JEP 514: Ahead-of-Time Command-Line Ergonomics
- JEP 515: Ahead-of-Time Method Profiling
- JEP 506: Scoped Values
- JEP 521: Generational Shenandoah
- JEP 474: Generational ZGC (finalized in Java 23)
- InfoQ: Java 25 Integrates Compact Object Headers
- Inside Java: Performance Improvements in JDK 25
- The Three Game-Changing Features of JDK 25
- Java 25 + JEP 519: Smaller Memory Footprint
Written for developers who remember when 30% performance gains required months of refactoring. Today, they require a single JVM flag. Welcome to Java 25.