Author: Hanno Embregts
Original post on Foojay: Read More
Table of Contents
- JEP 505: Structured Concurrency (Fifth Preview)
- JEP 506: Scoped Values
- JEP 507: Primitive Types in Patterns, instanceof, and switch (Third Preview)
- JEP 508: Vector API (Tenth Incubator)
- JEP 510: Key Derivation Function API
- JEP 511: Module Import Declarations
- JEP 512: Compact Source Files and Instance Main Methods
- JEP 513: Flexible Constructor Bodies
- JEP 519: Compact Object Headers
- JEP 521: Generational Shenandoah
It’s been six months since Java 24 was released, so it’s time for a fresh set of new Java features.
And the feature that immediately grabs the attention this time is stable values, taking Java’s support of immutability to the next level. Also, Java’s focus on improving performance intensifies, as more JEPs emerge from Project Leyden. On top of that, the Java Flight Recorder is now more equipped than ever to tackle performance issues!
This post takes you on a tour of everything that is part of this release, giving you a brief introduction to each of them. Where applicable the differences with Java 24 are highlighted and a few typical use cases are provided, so that you’ll be more than ready to start using these features after reading this.
Short descriptions of the repreviewed and finalized features are provided to prevent this article from becoming too lengthy. Each of these features comes with a link to a longer description should you wish to learn more.
JEP Overview
To start off, let’s look at an overview of the JEPs that ship with Java 25. This table contains the preview status for all JEP’s, to which project they belong, what kind of features they add and the things that have changed since Java 24.
JEP | Title | Status | Project | Feature Type | Changes since Java 24 |
---|---|---|---|---|---|
470 | PEM Encodings of Cryptographic Objects | Preview | Security Libs | Security | New feature |
502 | Stable Values | Preview | Core Libs | New API | New feature |
503 | Remove the 32-bit x86 Port | HotSpot | Deprecation | Removal | |
505 | Structured Concurrency | Fifth Preview | Loom | Concurrency | Major |
506 | Scoped Values | Loom | Concurrency | Minor | |
507 | Primitive Types in Patterns, instanceof, and switch | Third Preview | Amber | Language | None |
508 | Vector API | Tenth Incubator | Panama | New API | Minor |
509 | JFR CPU-Time Profiling | Experimental | HotSpot / JFR | Profiling | New feature |
510 | Key Derivation Function API | Security Libs | Security | None | |
511 | Module Import Declarations | Amber | Language | None | |
512 | Compact Source Files and Instance Main Methods | Amber | Language | Major | |
513 | Flexible Constructor Bodies | Amber | Language | None | |
514 | Ahead-of-Time Command-Line Ergonomics | Leyden | Performance | New feature | |
515 | Ahead-of-Time Method Profiling | Leyden | Performance | New feature | |
518 | JFR Cooperative Sampling | HotSpot / JFR | Profiling | New feature | |
519 | Compact Object Headers | HotSpot | Performance | None | |
520 | JFR Method Timing & Tracing | HotSpot / JFR | Profiling | New feature | |
521 | Generational Shenandoah | HotSpot / GC | Performance | Stability and performance improvements |
New features
Let’s start with the JEP’s that add brand-new features to Java 25.
Core Libs
Java 25 contains a single new feature that is part of the Core Libs:
- Stable Values (Preview)
JEP 502: Stable Values (Preview)
Immutable objects are a far less complicated concept than mutable objects, because they can only be in a single state and can be shared freely across multiple threads.
Currently, the main tool to achieve immutability in Java is final
fields.
But they come with two drawbacks, restricting their potential in many real-world applications:
- they must be set eagerly;
- the order in which multiple
final
fields are initialized can never be changed, as it is determined by the textual order in which the fields are declared.
Consider the use of immutability in the following code example, which takes place in a guitar store domain:
class OrderController { private final Logger logger = Logger.create(OrderController.class); void submitOrder(User user, List<Guitar> guitar) { logger.info("Ordering new guitars..."); // ... logger.info("New guitars have been ordered, let's get to work!"); } }
Whenever an instance of OrderController
is created, the logger
field is initialized eagerly, which potentially makes creating an OrderController
slow.
And this might not be the only place in our guitar store application where a logger
field is being initialized eagerly:
class GuitarStore { static final OrderController ORDERS = new OrderController(); static final GuitarRepository GUITARS = new GuitarRepository(); static final ManufacturerService MANUFACTURERS = new ManufacturerService(); }
All this initialization work causes the application to start up more slowly, and the worst thing is: it may not even be necessary! If a user is simply browsing the guitar store, with no intention of ordering a new guitar, the OrderController
won’t even be called and we will have initialized the logger
field for nothing.
Sacrificing Immutability For More Flexible Initialization
The only alternative we currently have is to resort to a mutability-based approach, in which we delay the initialization of complex objects to as late a time as possible:
class OrderController { private Logger logger; Logger getLogger() { if (logger == null) { logger = Logger.create(OrderController.class); } return logger; } void submitOrder(User user, List<Guitar> guitar) { getLogger().info("Ordering new guitars..."); // ... getLogger().info("New guitars have been ordered, let's get to work!"); } }
This improves application startup, but comes with a few drawbacks of its own:
- All accesses to the
logger
field must go through thegetLogger
method, but code that fails to follow this practice runs the risk of encounteringNullPointerException
s; - In multi-threaded environments, multiple logger objects could be created during concurrent calls to the
submitOrder
method; - Constant-folding access to an already-initialized
logger
field is no longer viable, as the JVM can’t trust its content never to change after its initial update.
What we need is a solution that has the best of both worlds:
- a way to promise that a field will be initialized by the time it is used,
- with a value that is computed at most once, and
- safely with respect to concurrency.
In other world, we want to defer immutability, and first-class support for it in the Java runtime.
Stable Values
JEP 502 introduces that first-class support in the form of stable values.
A stable value is an object of type StableValue
, that holds a single data value.
It must be initialized some time before its content is first retrieved, and it is immutable thereafter.
Let’s rewrite the OrderController
class to use a stable value for its logger:
class OrderController { private final StableValue<Logger> logger = StableValue.of(); Logger getLogger() { return logger.orElseSet(() -> Logger.create(OrderController.class)); } void submitOrder(User user, List<Guitar> guitar) { getLogger().info("Ordering new guitars..."); // ... getLogger().info("New guitars have been ordered, let's get to work!"); } }
After the call to StableValue.of()
, the stable value holds no content.
When it is accessed through the getLogger()
method, logger.orElseSet(...)
returns its content if the stable value was already set.
If it is unset, the orElseSet
method initializes it with the value supplied by the lambda expression.
The orElseSet
method also guarantees that the provided lambda expression is evaluated only once, even when it is invoked concurrently.
If we look at the properties of stable values, we see that they fill a gap between final and non-final fields:
Update count | Update location | Constant folding? | Concurrent updates? | |
---|---|---|---|---|
final field | 1 | Constructor or static initializer | Yes | No |
StableValue |
[0, 1] | Constructor or static initializer | Yes, after update | Yes, by winner |
non-final field | [0, ∞] | Anywhere | No | Yes |
Usage of stable values is certainly not limited to loggers–we can also use a stable value to store the OrderController
component itself, and related components:
class GuitarStore { static final StableValue<OrderController> ORDERS = StableValue.of(); static final StableValue<GuitarRepository> GUITARS = StableValue.of(); static final StableValue<ManufacturerService> MANUFACTURERS = StableValue.of(); public static OrderController orders() { return ORDERS.orElseSet(OrderController::new); } public static GuitarRepository guitars() { return GUITARS.orElseSet(GuitarRepository::new); } public static ManufacturerService manufacturers() { return MANUFACTURERS.orElseSet(ManufacturerService::new); } }
The application’s startup time improves because it no longer initializes its components, such as OrderController
, up front. Rather, it initializes each component on demand, via the orElseSet
method of the corresponding stable value. Each component, moreover, initializes its sub-components, such as its logger, on demand in the same way.
Under the hood, the JVM will treat the content of any stable value that is declared as final
as a constant, allowing constant-folding optimizations to happen.
Stable Suppliers
There’s one catch with our current approach: all access to the logger
stable value must go through the getLogger
method.
It would be more convenient if we could separate initializing a stable value from the actual initialization itself.
To this end, JEP 502 introduces stable suppliers, and this is how they work:
class OrderController { private final Supplier<Logger> logger = StableValue.supplier(() -> Logger.create(OrderController.class)); void submitOrder(User user, List<Guitar> guitar) { logger.get().info("Ordering new guitars..."); // ... logger.get().info("New guitars have been ordered, let's get to work!"); } }
Here, logger
is no longer a stable value, but a stable Supplier
. When a stable supplier is first created via StableValue.supplier(...)
, the content of the underlying stable value is not yet initialized. To access the logger, clients call logger.get()
, of which the first invocation will invoke the supplier and use its result to initialize the stable value. Subsequent invocations of logger.get()
will return the content immediately.
The resulting code is arguably more readable, because we no longer need a separate getLogger
method.
Stable Lists
What if you wanted to keep track of multiple stable values, for example when keeping a pool of objects? We can achieve this by using a stable list:
class GuitarStore { static final int POOL_SIZE = 10; static final List<OrderController> ORDERS = StableValue.list(POOL_SIZE, _ -> new OrderController()); public static OrderController orders() { long index = Thread.currentThread().threadId() % POOL_SIZE; return ORDERS.get((int) index); } }
Here, ORDERS
is no longer a stable value, but a stable list, where each element is the content of an underlying stable value. To access the content, clients call ORDERS.get(...)
, passing it an index, of which the first invocation will invoke the lamdba function that ignores the index and invokes the OrderController()
constructor. Subsequent invocations of ORDERS.get(...)
with the same index will return the element’s content immediately.
Preview Warning
Note that this JEP is in the preview stage, so you’ll need to add the --enable-preview
flag to the command-line to take the feature for a spin.
More Information
For more information on this feature, read JEP 502.
HotSpot
Java 25 introduces two new features in HotSpot:
- Ahead-of-Time Command-Line Ergonomics
- Ahead-of-Time Method Profiling
The HotSpot JVM is the runtime engine that is developed by Oracle. It translates Java bytecode into machine code for the host operating system’s processor architecture.
JEP 514: Ahead-of-Time Command-Line Ergonomics
Java 24 introduced an ahead-of-time cache to store classes in after reading, parsing, loading and linking them. A created cache for a specific application could then be re-used in subsequent runs of that application to improve startup time, by up to 42%.
In Java 24, creating such a cache took two runs of the java
process. The first one (the ‘training run’) would record its AOT configuration into the file app.aotconf
:
$ java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -cp app.jar com.example.App ...
…and the second one would use the configuration to create the cache into the file app.aot
:
$ java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot -cp app.jar
But it’s a little inconvenient that creating the cache is currently a two-step process.
On top of that, the AOT configuration file just sits there after creation, and isn’t needed any more once the cache has been created.
So that’s why JEP 514 introduces the command-line option AOTCacheOutput
, which performs a training run and creates an AOT cache in a single step.
$ java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App ...
As a convenience, when operating in this way the JVM creates a temporary file for the AOT configuration, deleting it when finished.
A production run that uses the AOT cache is started the same way as before:
$ java -XX:AOTCache=app.aot -cp app.jar com.example.App ...
For common cases this is a far more convenient way of creating AOT caches.
The ability to specify AOT modes and AOT configurations will be retained to support uncommon cases.
More Information
For more information on this feature, read JEP 514.
JEP 515: Ahead-of-Time Method Profiling
The total set of classes that must be loaded for a Java application to run can’t be predicted by the application’s author before starting it.
For example, new classes can be loaded in response to external input.
So to truly know what a Java application does, we must run it.
This observation is supported by Rice’s theorem, which states that “static analysis can always be defeated by program complexity”.
While running an application, the JVM can identify which methods do the important work, and how they do it. For an application to reach peak performance, the JVM’s just-in-time compiler must find the unpredictable set of ‘hot’ methods, i.e., those which consume the most CPU time, and compile their bytecode to native code.
Fun fact: this is actually how the “HotSpot JVM” got its name!
The HotSpot JVM has automatically collected this set of methods in the form of profiles since JDK 1.2. Unfortunately, there is a chicken-and-egg problem: an application cannot achieve peak performance until its method behaviors are predicted, and method behaviors cannot be predicted until the application has run for a significant period of time. This problem is currently solved by dedicating some resources to collecting profiles in the early part of an application’s run. During this ‘warmup period’ the application runs more slowly, until the JIT can compile the hot methods to native code. After warmup, no more methods need to be compiled unless the application changes its pattern of behavior, triggering a new warmup period.
JEP 515 proposes to improve warmup time by collecting profiles even earlier, in a training run of the application, allowing the application to rapidly achieve peak performance. To achieve this the AOT cache is extended to collect method profiles during training runs, that would otherwise be collected in the early part of an application’s run. Accordingly, production runs of the application are both faster to start and faster to achieve peak performance.
Note that profiles cached during training runs do not prevent additional profiling during production runs, since an application’s behavior in production can diverge from what was observed in training. Even with cached profiles, the HotSpot JVM continues to profile and optimize the application as it runs, fusing the benefits of AOT profiles, on-line profiling, and JIT compilation. The net effect of cached profiles is that the JIT runs earlier and with more accuracy, using the profiles to optimize the hot methods so that the application experiences a shorter warmup period. JIT tasks are inherently parallel, so the wall-clock time for warmup can be short when enough hardware resources are available.
To illustrate this, let’s look at a short program that uses the Stream API and thus causes almost 900 JDK classes to loaded.
About 30 hot methods are compiled at the highest optimization level:
import java.util.*; import java.util.stream.*; public class HelloStreamWarmup { static String greeting(int n) { var words = List.of("Hello", "" + n, "world!"); return words.stream() .filter(w -> !w.contains("0")) .collect(Collectors.joining(", ")); } public static void main(String... args) { for (int i = 0; i < 100_000; i++) greeting(i); System.out.println(greeting(0)); // "Hello, world!" } }
This program runs in 90 milliseconds with an AOT cache that contains no profiles. After collecting profiles into the AOT cache, it runs in 73 milliseconds — an improvement of 19%. The AOT cache with profiles occupies an additional 250 kilobytes, about 2.5% more than the AOT cache without profiles.
A short program such as this has only a short warmup period, but with cached profiles that warmup goes even faster as a result of timely and accurate JIT activity. More complex and longer-running programs are also likely to warm up more quickly, for the same reason.
More Information
For more information on this feature, read JEP 515.
Security Libs
Java 25 introduces a single new feature that is part of the Security Libs:
- PEM Encodings of Cryptographic Objects (Preview)
JEP 470: PEM Encodings of Cryptographic Objects (Preview)
Within a Java context, cryptographic objects such as public keys, private keys and certificates can be easily created and distributed. But outside of the Java world, the de facto standard is the Privacy-Enhanced Mail (PEM) format. Let’s see an example of a PEM-encoded cryptographic object:
-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEi/kRGOL7wCPTN4KJ2ppeSt5UYB6u cPjjuKDtFTXbguOIFDdZ65O/8HTUqS/sVzRF+dg7H3/tkQ/36KdtuADbwQ== -----END PUBLIC KEY-----
The Java Platform currently doesn’t include an easy-to-use API for decoding and encoding text in the PEM format, which means that decoding a PEM-encoded key can be a tedious job that involves careful parsing of the source PEM text. To further illustrate this point, encrypting and decrypting a private key currently requires over a dozen lines of code.
To solve this problem, JEP 470 introduces an API that can encode objects to the PEM format. It effectively acts as a bridge between Base64 and cryptographic objects. It involves a new interface and three new classes, in the java.security
package:
DEREncodable
: A sealed interface that groups together all cryptographic objects that support converting their instances to and from byte arrays in the Distinguished Encoding Rules (DER) format.
PEMEncoder
: A class that declares methods for encoding DEREncodable
objects into PEM text.
PEMDecoder
: A class that declares methods for decoding PEM text to DEREncodable
objects.
PEMRecord
: A record that implements DEREncodable
, which can hold any type of PEM data. It allows you to encode and decode PEM tests yielding cryptographic objects for which no Java representation currently exists.
Typical Usage
The following code example shows typical usage of the API:
PrivateKey privateKey = ...; PublicKey publicKey = ...; // let's encode a cryptographic object! PEMEncoder pemEncoder = PEMEncoder.of(); // this returns PEM text in a byte array byte[] privateKeyPem = pemEncoder.encode(privateKey); // this returns PEM text in a String String keyPairPem = pemEncoder.encodeToString(new KeyPair(privateKey, publicKey)); // this returns encrypted PEM text String password = "java-first-java-always"; String pem = pemEncoder.withEncryption(password).encodeToString(privateKey); // let's decode a cryptographic object! PEMDecoder pemDecoder = PEMDecoder.of(); // this returns a DEREncodable, so we need to pattern-match switch (pemDecoder.decode(pem)) { case PublicKey publicKey -> ...; case PrivateKey privateKey -> ...; default -> throw new IllegalArgumentException("Unsupported cryptographic object"); } // alternatively, if you know the type of the encoded cryptographic object in advance: PrivateKey key = pemDecoder.decode(pem, PrivateKey.class); // this decodes an encrypted cryptographic object PrivateKey decryptedkey = pemDecoder.withDecryption(password).decode(pem, PrivateKey.class);
Preview Warning
Note that this JEP is in the preview stage, so you’ll need to add the --enable-preview
flag to the command-line to take the feature for a spin.
More Information
For more information on this feature, read JEP 470.
Java Flight Recorder
Java 25 introduces three new features that are part of the Java Flight Recorder:
- JFR CPU-Time Profiling (Experimental)
- JFR Cooperative Sampling
- JFR Method Timing & Tracing
The Java Flight Recorder is an event recorder built into the JVM. It captures information about the JVM itself – and the applications running in it – not unlike a data flight recorder (or ‘black box’) in a commercial aircraft.
JEP 509: JFR CPU-Time Profiling (Experimental)
Profiling is the act of measuring the consumption of computational resources such as memory, CPU cycles and elapsed time. The resulting measurements can help make a program more efficient, by identifying which program elements to optimize. One would typically prioritize optimizing those elements that consume the most resources.
The Java Flight Recorder (or JFR) is the JDK’s profiling and monitoring facility, commonly used to profile heap memory and CPU usage. Its support of heap allocation profiling is good, but its implementation of CPU profiling currently comes with a few drawbacks:
- it’s only able to approximate CPU-cycle consumption by emitting a sample of running Java threads (in the form of a stacktrace) in a JFR event at regular time intervals (say, every 20 ms);
- it doesn’t include threads that are running native code;
- obtaining the sample may fail without reporting it;
- the sample contains a subset of running threads only.
This means that the resulting profile may be inaccurate and not reflect the actual CPU usage profile, and this effect is amplified when sample collecting occurs over a relatively short period of time.
Towards More Accurate Measurements
Version 2.6.12 of the Linux kernel added the ability to accurately measure CPU-cycle consumption through a timer that emits signals at fixed intervals of elapsed CPU time (rather than real time). JEP 509 enhances the JFR to make use of this timer, producing more accurate CPU-time profiles than could be obtained through the current sampling approach. On top of that, also CPU cycles that are consumed by Java applications running native code would be correctly tracked.
Usage
JFR will use Linux’s CPU-timer mechanism to sample the stack of every thread running Java code at fixed intervals of elapsed CPU time. Each such sample is recorded in a new type of event, called jdk.CPUTimeSample
. This event is not enabled by default.
Here’s how to enable the event when running the JFR:
$ java -XX:StartFlightRecording=jdk.CPUTimeSample#enabled=true,filename=profile.jfr ...
Note that his feature will currently only be available on Linux systems.
CPU-time profiling may be added to the JFR on other platforms in the future.
More Information
For more information on this feature, read JEP 509. It contains a few more details on how to use the new JFR event and what a typical flame graph would look like.
JEP 518: JFR Cooperative Sampling
We learned in the previous section that the JFR collects samples by obtaining stacktraces for a number of running Java threads. In order to produce these stacktraces, the target threads must be suspended so that the call frames on the stack can be parsed. As part of that process, the Hotspot JVM maintains metadata that is valid only when the thread is suspended at well-defined code locations known as safepoints. However, if sampling is only done at safepoints, the notorious safety bias problem occurs — where accuracy is lost since frequently-executed pieces of code might not be anywhere near a safepoint.
To avoid this problem, the JFR currently samples the stacks of program threads asynchronously, suspending threads at code locations that may not be safepoints at all. This means the metadata maintained by the JVM may not be valid and so we have to resort to using heuristics to generate a stacktrace. Unfortunately, these heuristics are inefficient, and may even crash the JVM when their results are incorrect.
JEP 518 proposes to avoid the need for these heuristics by parsing thread stacks only at safepoints.
It comes with a different approach to avoid the safepoint bias problem: taking samples cooperatively. When it is time to take a sample, JFR’s sampler thread still suspends the target thread. But rather than attempting to parse the stack, it just records the target’s program counter and stack pointer in a sample request, which it appends to an internal thread-local queue. It then arranges for the target thread to stop at its next safepoint, and resumes the thread.
The target thread now runs normally until its next safepoint. At that time, the safepoint handling code inspects the queue. If it finds any sample requests, then, for each one, it reconstructs a stack trace, adjusting for safepoint bias, and emits a JFR execution-time sampling event.
More Information
For more information on this feature, read JEP 518.
JEP 520: JFR Method Timing & Tracing
When performance-related problems arise, knowing how much time is spent in which code unit can be very valuable. You may have just introduced a method that takes particularly long to execute.
Or a static initializer may cause your application to take an unusually long time to start. In such cases, during development, execution of these code units can be analyzed by using debuggers or the Java Microbenchmark Harness.
However, during testing and production, far less feasible options exist. Sample-based profilers can capture stack traces for frequently executed methods, but can’t provide timing and tracing for all invocations. And the JDK Mission Control tool can certainly instrument methods to emit JFR events, but not without significant performance overhead.
JEP 520 introduces two new JFR events (jdk.MethodTiming
and jdk.MethodTrace
) that both accept a filter to select the methods to time and trace.
For example, to see what triggers the resize of a HashMap
, you can configure the MethodTrace
event’s filter when making a recording and then use the jfr
tool to display the recorded event:
$ java -XX:StartFlightRecording:jdk.MethodTrace#filter=java.util.HashMap::resize,filename=recording.jfr ... $ jfr print --events jdk.MethodTrace --stack-depth 20 recording.jfr jdk.MethodTrace { startTime = 00:39:26.379 (2025-03-05) duration = 0.00113 ms method = java.util.HashMap.resize() eventThread = "main" (javaThreadId = 3) stackTrace = [ java.util.HashMap.putVal(int, Object, Object, boolean, boolean) line: 636 java.util.HashMap.put(Object, Object) line: 619 sun.awt.AppContext.put(Object, Object) line: 598 sun.awt.AppContext.<init>(ThreadGroup) line: 240 sun.awt.SunToolkit.createNewAppContext(ThreadGroup) line: 282 sun.awt.AppContext.initMainAppContext() line: 260 sun.awt.AppContext.getAppContext() line: 295 sun.awt.SunToolkit.getSystemEventQueueImplPP() line: 1024 sun.awt.SunToolkit.getSystemEventQueueImpl() line: 1019 java.awt.Toolkit.getEventQueue() line: 1375 java.awt.EventQueue.invokeLater(Runnable) line: 1257 javax.swing.SwingUtilities.invokeLater(Runnable) line: 1415 java2d.J2Ddemo.main(String[]) line: 674 ] }
As you can see, the filter is specified just like a method reference.
As the JVM starts up, it instruments the targeted method by injecting bytecode to emit a MethodTrace
event.
Configuration Files
The JFR is usually configured via a configuration file, which has now also been enhanced to support method timing and tracing. Additionally, the jfr view
and jcmd JFR.view
commands have been enhanced to display method timing and tracing results.
To put this all together, if an application suffers from slow startup, timing the execution of all static initializers may suggest where lazy initialization could be used. We can time all static initializers in all classes by omitting the class name, specifying ::
as the filter:
$ java '-XX:StartFlightRecording:method-timing=::<clinit>,filename=clinit.jfr' ... $ jfr view method-timing clinit.jfr Method Timing Timed Method Invocations Average Time ------------------------------------------------------ ----------- ------------ sun.font.HBShaper.<clinit>() 1 32.500000 ms java.awt.GraphicsEnvironment$LocalGE.<clinit>() 1 32.400000 ms java2d.DemoFonts.<clinit>() 1 21.200000 ms java.nio.file.TempFileHelper.<clinit>() 1 17.100000 ms sun.security.util.SecurityProviderConstants.<clinit>() 1 9.860000 ms java.awt.Component.<clinit>() 1 9.120000 ms sun.font.SunFontManager.<clinit>() 1 8.350000 ms sun.java2d.SurfaceData.<clinit>() 1 8.300000 ms java.security.Security.<clinit>() 1 8.020000 ms sun.security.util.KnownOIDs.<clinit>() 1 7.550000 ms ...
Filtering on Classes and Annotations
To time or trace multiple methods, a filter can mention a class or an annotation.
For example, to see the number of times that a Jakarta REST endpoint is invoked, and measure the approximate execution time:
$ jcmd <pid> JFR.start method-timing=@jakarta.ws.rs.GET
Multiple filters can be specified, separated by semicolons.
Benefits
This new approach comes with both performance and usability benefits. As we have seen, the JVM can filter methods, eliminating the need to parse the bytecode of every loaded class twice. And because can methods be timed and traced without having to configure or install an agent, usability improves as well.
More Information
For more information on this feature, read JEP 520.
Repreviews and Finalizations
Now it’s time to take a look at a few features that may already be familiar to you, because they were introduced in a previous version of Java. They have been repreviewed (or finalized) in Java 25, with only minor changes compared to Java 24 in most cases. Therefore, to avoid a very lengthy article, we’ll outline these changes and link to a previous article for a full feature description, should you wish to refresh your memory.
JEP 505: Structured Concurrency (Fifth Preview)
Structured concurrency treats groups of related tasks running in different threads as a single unit of work, thereby streamlining error handling and cancellation, improving reliability, and enhancing observability.
What’s Different From Java 24?
In Java 25, a StructuredTaskScope
can be opened via static factory methods rather than through public constructors. The zero-parameter open()
factory method covers the common case by creating a StructuredTaskScope
that waits for all subtasks to succeed or any subtask to fail. Other policies and outcomes can be implemented by providing an appropriate Joiner
to one of the richer open(Joiner)
factory methods.
Preview Warning
Note that this JEP is in the preview stage, so you’ll need to add the --enable-preview
flag to the command-line to take the feature for a spin.
More Information
If you prefer to get more information on the current state of this feature, then read JEP 505 or the full feature description from a previous article.
JEP 506: Scoped Values
Scoped values enable the sharing of immutable data within and across threads.
They are preferred to thread-local variables, especially when using a large number of (virtual) threads.
What’s Different From Java 24?
A single change was made to the API compared to Java 24:
- The
ScopedValue.orElse()
method no longer acceptsnull
as its argument.
On top of that, the preview status has been dropped, which means the scoped values API is now finalized!
More Information
For more information on this feature, read JEP 506 or the full feature description from a previous article.
JEP 507: Primitive Types in Patterns, instanceof, and switch (Third Preview)
Pattern matching now supports primitive types in all pattern contexts. On top of that, the instanceof
and switch
constructs have been extended to also work with all primitive types.
What’s Different From Java 24?
Compared to the preview version of this feature in Java 24, nothing was changed or added. JEP 507 simply exists to gather more feedback from users.
Preview Warning
Note that this JEP is in the preview stage, so you’ll need to add the --enable-preview
flag to the command-line to take the feature for a spin.
More Information
For more information on this feature, read JEP 507 or the full feature description from a previous article.
JEP 508: Vector API (Tenth Incubator)
The Vector API makes it possible to express vector computations that reliably compile at runtime to optimal vector instructions.
This means that these computations will significantly outperform equivalent scalar computations on the supported CPU architectures (x64 and AArch64).
What’s Different From Java 24?
The following changes were made to the Vector API compared to Java 23:
VectorShuffle
now supports access to and fromMemorySegment
;- The implementation now links to native mathematical-function libraries via the Foreign Function & Memory API (JEP 454) rather than custom C++ code inside the HotSpot JVM, thereby improving maintainability;
- Addition, subtraction, division, multiplication, square root, and fused multiply/add operations on
Float16
values are now auto-vectorized on supporting x64 CPUs.
The Vector API will keep incubating until necessary features of Project Valhalla become available as preview features. When that happens, the Vector API will be adapted to use them, and it will be promoted from incubation to preview.
More Information
For more information on this feature, read JEP 508 or the full feature description from a previous article.
JEP 510: Key Derivation Function API
To be able to withstand practical quantum computing attacks, it is Java’s long-term goal is to eventually implement Hybrid Public Key Encryption (HPKE), which facilitates a seamless transition to quantum-resistant encryption methods. To that end, Java 24 introduced a new Key Derivation Function API, which is now finalized in Java 25.
Key derivation functions are cryptographic algorithms for deriving additional keys from a secret key and other data. A KDF allows keys to be created in a manner that is both secure and reproducible by two parties sharing knowledge of the inputs. Deriving keys is similar to hashing passwords. A KDF employs a keyed hash along with extra entropy from its other inputs to either derive new key material or safely expand existing values into a larger quantity of key material.
What’s Different From Java 24?
Compared to the preview version of this feature in Java 24, the preview status has been dropped, which means the Key Derivation Function API is now finalized!
More Information
For more information on this feature, read JEP 510 or the full feature description from a previous article.
JEP 511: Module Import Declarations
Module import declarations import all of the public top-level classes and interfaces in the packages exported by that module. They are a shorter alternative for listing many imports that originate from the same root package.
What’s Different From Java 24?
This feature was in second preview in Java 24, and in Java 25 the preview status has been dropped. This means module import declarations are now finalized!
More Information
For more information on this feature, read JEP 511 or the full feature description from a previous article.
JEP 512: Compact Source Files and Instance Main Methods
Comapct source files allow developers to write Java programs without the need to explicitly declare a class. They can contain ‘instance main methods’: a shorter form of the classic main()
method without requiring program arguments or imports. These two features simplify the process of writing small programs and scripts by reducing boilerplate code.
What’s Different From Java 24?
The feature that used to be known as ‘simple source files’ was renamed to ‘compact source files’.
On top of that, several minor improvements are now in place based on developer feedback:
- The new
IO
class for basic console I/O is now in thejava.lang
package rather than thejava.io
package. Thus it is implicitly imported by every source file. - The implementation of the
IO
class is now based uponSystem.out
andSystem.in
rather than thejava.io.Console
class. - The static methods of the
IO
class are no longer implicitly imported into compact source files. Thus invocations of these methods must name the class, e.g.,IO.println("Hello, world!")
, unless the methods are explicitly imported.
This last change has been made to make a beginner’s first experience with Java a bit easier. When the static methods of the IO
class were automatically imported, this had the pleasing effect of making the methods in IO
appear to be built-in to the Java language. However, to evolve a compact source file into an ordinary source file, a beginner would have to add a static import declaration – an advanced concept that a beginner should definitely not tackle on their first day.
More Information
For more information on this feature, read JEP 512 or the full feature description from a previous article.
JEP 513: Flexible Constructor Bodies
Flexible constructor bodies allow statements to appear before an explicit constructor invocation, like super(..)
or this(..)
. The statements cannot reference the instance under construction, but they can initialize its fields. Initializing fields before invoking another constructor makes a class more reliable when methods are overridden.
What’s Different From Java 24?
Compared to the preview version of this feature in Java 24, the preview status has been dropped, which means flexible constructor bodies are now finalized!
More Information
For more information on this feature, read JEP 513 or the full feature description) from a previous article.
JEP 519: Compact Object Headers
JDK 24 introduced compact object headers as an experimental feature, which enabled a reduction of the object header size to 64 bits. Since then, compact object headers have proven their stability and performance. They have been tested at Oracle by running the full JDK test suite. They have also been tested at Amazon by hundreds of services in production, most of them using backports of the feature to JDK 21 and JDK 17. On top of that, various other experiments have demonstrated that enabling compact object headers improves performance.
What’s Different From Java 24?
The experimental status has been dropped, which means compact object headers have now become a product feature. They can be enabled via the command-line options:
$ java -XX:+UseCompactObjectHeaders ...
This means the -XX:+UnlockExperimentalVMOptions
option that was required in Java 24 is no longer necessary. In later releases, we can expect the feature to become enabled by default. Eventually the code for legacy object headers will be removed altogether.
More Information
For more information on this feature, including references to the conducted experiments that proved the better performance, read JEP 519 or the full feature description from a previous article.
JEP 521: Generational Shenandoah
The Shenandoah garbage collector is an ultra-low pause time garbage collector. It has been available for production use since Java 15 and has been designed to dramatically reduce garbage collection pause times, regardless of the heap size that is used. It can achieve these low pause times because most of the work is done before the GC pause, in a series of preparation steps. Shenandoah marks and compacts any heap objects eligible for garbage collection, while regular Java user threads are still running.
Java 24 introduced an experimental extension to Shenandoah that maintains separate generations for young and old objects, allowing Shenandoah to collect young objects more frequently. This results in a significant performance gain for applications running with generational Shenandoah, without sacrificing any of the valuable properties that the garbage collector is already known for.
The reason for handling young and old objects separately stems from the weak generational hypothesis, which states that young objects tend to die young, while old objects tend to stick around. This means that collecting young objects requires fewer resources and yields more memory, while collecting old objects requires more resources and yields less memory. This is the reason we can improve the performance of applications that use Shenandoah by collecting young objects more frequently.
What’s Different From Java 24?
The experimental status has now been dropped, which means generational mode has now become a product feature. Compared to JDK 24, many stability and performance improvements have been implemented, and extensive testing on multiple platforms has been performed.
To run your workload with generational Shenandoah in Java 25, the following configuration is needed:
$ java ... -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational
This means the -XX:+UnlockExperimentalVMOptions
option that was required in Java 24 is no longer necessary. Note that no changes were made to Shenandoah’s default behaviour. This may still change in a future release, though.
More Information
For more information on this feature, read JEP 521 or the full feature description from a previous article.
Deprecations, Removals & Restrictions
Java 25 comes with a single removal.
JEP 503: Remove the 32-bit x86 Port
This JEP removes the 32-bit x86 (Linux) port, which was to be expected after its deprecation in Java 24. The affected users are expected to already have migrated to 64-bit JVMs.
Supporting multiple platforms has been the focus of the Java ecosystem since the beginning. But older platforms cannot be supported indefinitely—the effort that was required to maintain this port exceeded its advantages. Keeping it up-to-date with new features like Loom, the Foreign Function & Memory API (FFM), the Vector API, and late GC barrier expansion represented a significant cost. So it’s time to say goodbye to this port!
More Information
For more information on this removal, read JEP 503.
Final thoughts
And that concludes our discussion of the 18 JEP’s that come with Java 25. But that’s not even all that’s new: many other updates were included in this release, including various performance, stability and security updates. One thing is for sure: this version of Java is ready to perform to the limit. So what are you waiting for? It’s time to take this brand-new Java release for a high-performance spin!
The post Here’s Java 25, Ready to Perform to the Limit appeared first on foojay.