00:17:00
The journey from Java Development Kit (JDK) 21 to JDK 25 brings significant enhancements in performance, garbage collection, and observability tools. These updates address critical issues like virtual thread pinning and introduce features like compact object headers, making Java applications faster, more efficient, and easier to monitor. Here’s a detailed look at what’s new.
Virtual threads, introduced in JDK 21, were designed to improve scalability by allowing lightweight concurrency. However, a known issue occurred when virtual threads encountered blocking operations inside synchronized
blocks or methods—they would pin the underlying platform thread, hindering scalability. This problem was highlighted in real-world scenarios, such as those reported by Netflix.
JDK 24 resolved this with JEP 491: "Synchronized Virtual Threads Without Pinning." Upgrading to JDK 25 includes this fix, ensuring virtual threads properly release platform threads during synchronized blocks, eliminating a major obstacle for high-concurrency applications.
JDK 25 delivers noticeable performance gains over JDK 21, even without code changes. Testing with the Spring Boot Pet Clinic application shows:
These improvements stem from cumulative optimizations across JDK versions, including string handling enhancements (e.g., JEP 451: String Deduplication in JDK 25) and garbage collector tweaks. However, results vary based on application architecture and workload.
JDK 25 may use more heap memory by default due to optimistic internal heuristics. In tests, committed heap space increased from ~400 MB on JDK 21 to ~480 MB on JDK 25. Setting a max heap size (e.g., 400 MB) had minimal impact on throughput, as actual usage remained around 240–250 MB. This suggests that while JDK 25 is more aggressive with memory allocation, it can be managed with appropriate JVM flags.
A new feature in JDK 25, compact object headers (from Project Liliput), reduces the size of object headers on 64-bit HotSpot JVMs from 128 bits to 64 bits. This can decrease heap usage and garbage collection pressure, potentially lowering latency. Enable it with the -XX:+CompactObjectHeaders
flag.
However, performance impacts are mixed: throughput may decrease in some cases due to object layout changes. It’s recommended to test this feature based on your application’s needs, weighing memory savings against throughput requirements.
ZGC now exclusively uses generational mode in JDK 25, deprecating the single-generation mode introduced in JDK 21. Generational ZGC improves CPU and memory efficiency for most web applications, offering shorter pause times—often in the microsecond range—at the cost of slightly reduced throughput. This makes it ideal for latency-sensitive environments like microservices.
In tests, ZGC processed ~6,300 requests compared to G1GC’s ~7,500, but pause times were significantly shorter, enhancing user experience in responsive applications.
JFR is a built-in diagnostic tool for tracking performance issues, resource leaks, and method execution with minimal overhead. JDK 25 adds two key updates:
For example, to trace Spring Data methods, use commands like method-timing
or method-trace
with fully qualified method names. JFR’s low impact makes it suitable for continuous production monitoring.
Upgrading to JDK 25 brings tangible benefits: fixed virtual threads, better performance, memory optimizations, and advanced observability with JFR. While some features like compact object headers require testing, the overall improvements make it a worthwhile update for most Java applications.