Core Java

JDK 26 Performance Enhancements

Java Development Kit (JDK) 26 introduces improvements across Java libraries, garbage collection, startup performance, compilation, and runtime execution. Released in March 2026, this feature release includes optimizations that can help applications improve throughput, reduce startup overhead, and handle concurrent workloads more efficiently. Some improvements benefit existing applications without requiring code changes, while others introduce new APIs or require application-level adoption. The actual performance gains depend on the workload, hardware, JVM configuration, and application architecture.

1. JDK Library Improvements

1.1 Lazy Constants

JEP 526 introduces the Lazy Constants API as a second preview. A lazy constant allows a value to be initialized when it is first needed and then remain unmodifiable. Unlike a conventional final field initialized during object construction or class initialization, a lazy constant defers initialization until the value is accessed. After initialization, the JVM can treat the value as a constant and apply suitable optimizations. This approach can be useful for expensive configuration objects, lookup tables, and resources that are not required during every application startup. Because Lazy Constants are a preview API in JDK 26, they require preview-feature enablement and may change in future releases.

1.2 HTTP/3 Support

JEP 517 adds HTTP/3 support to the Java HTTP Client API. HTTP/3 uses QUIC over UDP instead of TCP, which can improve connection establishment and reduce the impact of packet loss in suitable network conditions. Applications using java.net.http.HttpClient can explore HTTP/3 without replacing their entire HTTP client implementation. The performance benefit depends on server support, network conditions, TLS setup, and connection reuse. Developers should benchmark HTTP/3 against HTTP/2 using representative application traffic.

1.3 Vector API

The Vector API continues as an incubator API in JDK 26. It allows developers to express operations on multiple values in a single vector operation, enabling the JVM to use suitable CPU vector instructions. This can benefit numerical calculations, image processing, scientific computing, and data transformations. However, existing Java code does not automatically become vectorized merely because the API is available. Developers must use the API explicitly and benchmark it against a scalar implementation.

2. Garbage Collection and Startup Improvements

2.1 G1 Garbage Collector Throughput

JEP 522 improves G1 garbage collector throughput by reducing synchronization between application threads and garbage collector threads. G1 tracks reference updates using card tables. Maintaining these tables can introduce synchronization overhead, particularly in applications that frequently modify object references. JDK 26 changes card-table handling to reduce synchronization overhead while background processing operates independently. This can improve throughput in workloads involving frequent allocations and reference updates. Applications already using G1 can benefit without changing application code or switching garbage collectors.

2.2 Ahead-of-Time Object Caching

JEP 516 extends ahead-of-time (AOT) object caching to work with any garbage collector, including ZGC. AOT caching allows the JVM to reuse eligible cached object state during startup and warmup instead of recreating all of it through the usual execution path. The garbage-collector-independent object representation makes AOT object caching more flexible for applications using different garbage collection strategies. The benefit depends on which objects can be cached and the application’s startup profile.

2.3 Smaller Default Initial Heap

JDK 26 changes the default initial heap sizing behavior when an application does not explicitly configure -Xms or -XX:InitialHeapSize. The JVM starts with the minimum heap size and can grow it as required, rather than using the previous default initial heap calculation based on InitialRAMPercentage. This can reduce startup work and memory initialization overhead, particularly for short-lived command-line applications and serverless workloads. Long-running applications should still be tested with suitable heap settings because heap growth and garbage collection behavior depend on workload characteristics.

3. Compiler and Runtime Improvements

3.1 Improved C2 Compilation

The C2 compiler is the JVM’s optimizing just-in-time compiler. It generates optimized machine code for frequently executed methods. JDK 26 includes an improvement that enables C2 compilation for methods with large numbers of parameters. Previously, certain methods could fail C2 compilation and fall back to less optimized execution. This change allows affected methods to benefit from C2 optimizations without requiring application code changes. The impact is most relevant when such methods appear on frequently executed code paths.

3.2 Virtual Thread Improvements

Virtual threads allow Java applications to handle large numbers of concurrent tasks without creating a separate operating-system thread for every task. JDK 26 improves virtual-thread behavior when a virtual thread encounters a class initializer already being executed by another thread. In most cases, the waiting virtual thread can unmount from its carrier thread instead of pinning that carrier while waiting. This releases the underlying platform thread to execute other work and can improve scalability when many virtual threads wait for class initialization to finish. The benefit depends on the application’s concurrency patterns. It does not mean every blocking operation or virtual thread automatically becomes faster.

4. Measuring the Upgrade

The best way to evaluate JDK 26 is to measure the same application on the current production JDK and JDK 26 under equivalent conditions. Important performance metrics include:

  • Startup time: Time required for the application to become ready.
  • Throughput: Number of operations completed per second.
  • Latency: Average and percentile response times, particularly p95 and p99.
  • Memory usage: Heap occupancy, allocation rate, and total process memory.
  • Garbage collection: Pause duration, collection frequency, and GC CPU overhead.

4.1 Java Code Example

The following Java program measures the time required to process one million integers. It performs warmup iterations before measuring execution time, making it a useful starting point for comparing runtime behavior across JDK versions. Save the code as Jdk26PerformanceDemo.java.

import java.util.Random;

public class Jdk26PerformanceDemo {

    private static final int SIZE = 1_000_000;
    private static final int WARMUP_ROUNDS = 5;
    private static final int MEASURED_ROUNDS = 10;

    public static void main(String[] args) {
        int[] numbers = new int[SIZE];
        Random random = new Random(42);

        // Generate repeatable input data.
        for (int i = 0; i < SIZE; i++) {
            numbers[i] = random.nextInt(1_000);
        }

        // Warm up the JVM and JIT compiler.
        for (int i = 0; i < WARMUP_ROUNDS; i++) {
            calculateChecksum(numbers);
        }

        long totalNanos = 0;
        long checksum = 0;

        // Measure repeated executions.
        for (int i = 0; i < MEASURED_ROUNDS; i++) {
            long start = System.nanoTime();

            checksum = calculateChecksum(numbers);

            long elapsed = System.nanoTime() - start;
            totalNanos += elapsed;

            System.out.printf(
                "Round %d: %.3f ms%n",
                i + 1,
                elapsed / 1_000_000.0
            );
        }

        double averageMillis =
            totalNanos / (double) MEASURED_ROUNDS / 1_000_000.0;

        System.out.printf("Checksum: %d%n", checksum);
        System.out.printf("Average time: %.3f ms%n", averageMillis);
    }

    private static long calculateChecksum(int[] numbers) {
        long sum = 0;

        for (int number : numbers) {
            sum += (long) number * number;
        }

        return sum;
    }
}

4.1.1 Code Explanation

This Java program measures the execution time of a simple computational task to help compare JVM performance across Java versions. The SIZE, WARMUP_ROUNDS, and MEASURED_ROUNDS constants define an array of one million integers, five warmup iterations, and ten measured iterations, respectively. The program uses Random(42) to generate repeatable input data. Before measuring performance, it executes calculateChecksum() five times to give the JVM’s Just-In-Time (JIT) compiler an opportunity to optimize frequently executed code. During each measured iteration, System.nanoTime() records the start time, the checksum method calculates the sum of the squared array elements, and the elapsed time is accumulated and displayed in milliseconds. The calculateChecksum() method uses a long variable to reduce the risk of integer overflow when adding squared values. Finally, the program prints the checksum and average execution time across all ten measured iterations. Running the same program on different JDK versions with equivalent configurations provides an initial performance comparison, although reliable benchmarking requires repeated runs and a framework such as JMH.

4.1.2 Code Run and Output

Compile and run the program using JDK 26: java Jdk26PerformanceDemo. Verify the installed Java version using: java -version. Repeat the test using the previous JDK with the same source code, input size, JVM options, and hardware.

Round 1: 2.145 ms
Round 2: 1.982 ms
Round 3: 2.010 ms
Round 4: 1.954 ms
Round 5: 1.967 ms
Round 6: 1.943 ms
Round 7: 1.956 ms
Round 8: 1.949 ms
Round 9: 1.938 ms
Round 10: 1.951 ms
Checksum: 332833782500
Average time: 1.980 ms

The program is compiled and executed using JDK 26, and java -version verifies the installed Java version. The output displays the execution time for each of the ten measured rounds in milliseconds, allowing developers to observe variations between iterations. The checksum confirms that the same computation is performed on the input data, while the average time summarizes the execution duration across all measured rounds. In this illustrative output, the average execution time is 1.980 milliseconds. These timing values are examples rather than actual benchmark results. To evaluate JDK 26 performance improvements, run the same program with the previous JDK under equivalent hardware and JVM settings, then compare the average execution times. A lower execution time indicates better performance for this specific workload, but further benchmarking is needed to draw reliable conclusions about overall JVM performance.

5. Conclusion

JDK 26 introduces performance-related improvements across libraries, garbage collection, startup, compilation, and virtual-thread execution. G1’s reduced synchronization can improve throughput, AOT object caching supports more garbage collectors, and the smaller default initial heap can reduce startup overhead. Compiler and virtual-thread improvements can also benefit applications that exercise the affected execution paths. Library enhancements such as Lazy Constants, HTTP/3 support, and the Vector API provide additional opportunities to improve performance when adopted appropriately. Before upgrading, establish a baseline, run correctness and compatibility tests, and benchmark representative workloads. Measuring throughput, latency, startup time, memory consumption, and garbage collection behavior provides a reliable basis for deciding whether JDK 26 delivers meaningful improvements for a particular application.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button