Java 26 with JVM optimizations, HTTP/3, and finally no Applet API

The current OpenJDK 26 is strategically important and not only brings exciting innovations but also eliminates legacy issues like the outdated Applet API.

listen Print view
Java coffee beans

(Image: Natalia Hanin / Shutterstock.com)

19 min. read
By
  • Falk Sippach
Contents

The current version, OpenJDK 25, was released in the fall of 2025, with many vendors offering Long-Term Support (LTS). Many companies use such releases as a stability anchor for migrations and long-term planning. Java 26, on the other hand, is again a regular six-month release and thus a step in the continuous evolution of the platform: language, runtime, and standard library are systematically modernized without compromising the stability and backward compatibility for which Java has been known for decades.

Falk Sippach
Falk Sippach

Falk Sippach is a software architect, consultant, and trainer at embarc Software Consulting GmbH. He is actively involved in the Java community and shares his knowledge in articles, blog posts, and lectures.

Overall, Java 26 contains ten JEPs (JDK Enhancement Proposals). Some of them continue known developments, for example in Pattern Matching, Structured Concurrency, or the Vector API. Other changes concern the performance of the JVM, new network protocols, or improvements in the security stack. In addition, there are some cleanup tasks in the JDK, such as the final removal of the old Applet API.

Even if Java 26 appears unspectacular at first glance, its strategic importance should not be underestimated. Some changes prepare the platform for larger developments planned for the coming years. These include, in particular, Project Valhalla with Value Types, stronger integrity of the object model under the guiding principle of “Integrity by Default,” and optimizations for modern hardware, AI, and cloud workloads.

The java.net.http.HttpClient introduced in Java 11 has become the standard tool for HTTP communication in Java applications over the past few years. With Java 26, this API now also supports HTTP/3 (JEP 517). HTTP/3 no longer relies on TCP but on QUIC, a UDP-based transport protocol. This offers several advantages over HTTP/1.1 and HTTP/2:

  • lower latency in connection establishment,
  • better performance in case of packet loss,
  • no Head-of-Line blocking between parallel streams, and
  • more stable connections during network changes, such as with mobile clients.

HTTP/3 can bring noticeable advantages, especially in cloud environments or with globally distributed applications. For developers, however, the use of the API remains largely unchanged. They can simply specify the desired HTTP version when creating the client:

HttpClient client = HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_3)
        .build();

HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://example.com/api"))
        .GET()
        .build();

HttpResponse<String> response =
        client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());

With JEP 526, Java 26 brings another iteration of the so-called Lazy Constants. The feature was already included in Java 25 under the name Stable Values and now appears as a second preview with a revised API. The idea behind it is simple: constants should only be calculated when the application needs them. Previously, the JVM created final fields during class initialization. This is efficient if the application uses the values, but costs time during startup. In practice, objects that are expensive to create may be included, which an application may never need or only much later. Therefore, it makes sense to initialize them later. A typical example is prepared regular expressions or lookup structures:

static final Map<String, Pattern> PATTERNS = Map.of(
        "email", Pattern.compile("..."),
        "phone", Pattern.compile("..."),
        "zip", Pattern.compile("...")
);

When the class is loaded, all patterns are compiled immediately here, regardless of whether they are used later or not. Implementing such an initialization lazily is difficult. It must be thread-safe and, at the same time, generate as little synchronization overhead as possible. A classic approach would be the so-called Double-Checked Locking:

private volatile Logger logger;

Logger logger() {
    if (logger == null) {
        synchronized (this) {
            if (logger == null) {
                logger = Logger.create(OrderController.class);
            }
        }
    }
    return logger;
}

Such constructs are not only error-prone but, above all, difficult to read and quickly lead to subtle concurrency problems. Lazy Constants solve this problem directly at the platform level:

private final LazyConstant<Logger> logger = LazyConstant.of(() -> Logger.create(OrderController.class));

void submitOrder(User user) {
    logger.get().info("order started");
}

The JVM handles thread-safe initialization on first access, so developers do not need to implement their synchronization mechanisms. The JVM also has the ability to perform optimizations. This can also lead to performance improvements in later Java releases.

The current preview brings a significantly simplified API compared to the first draft. Low-level methods like orElseSet() or trySet() are no longer included. Instead, the API now focuses on factory methods that generate content via a calculation function. Additionally, the name change from StableValue to LazyConstant is intended to describe the purpose more clearly: a constant value that is only generated when needed. This can bring advantages, especially in cloud and microservice architectures. Applications start faster, unnecessary object allocations are avoided, and memory usage in the early runtime phase of an application decreases.

In addition to new APIs, Java 26 includes several improvements to the runtime environment. These primarily target two typical requirements of modern applications: high throughput under parallel load and faster startup behavior in cloud environments.

Videos by heise

The Garbage Collector (GC) G1 has been the standard GC for the JVM for many years. Its strength lies in relatively stable pause times with good throughput. However, in systems with many CPU cores, G1 has so far encountered internal scaling limits because certain operations were heavily synchronized. With JEP 522, Java 26 specifically reduces these synchronization points. This results in less lock contention in the garbage collector, and parallelization can be better utilized. Applications with many threads and high object allocation, such as highly parallel backend systems or data-intensive batch workloads, benefit particularly from this.

Another step towards faster startups is JEP 516. This involves a mechanism that allows certain objects to be created and reused in advance. Typical candidates for this include:

  • Lookup tables,
  • configuration structures,
  • metadata objects, and
  • frequently used string or data structures.

Instead of rebuilding these structures every time, they can be prepared and reused directly at startup. This reduces initialization work and shortens the warm-up phase of an application. An important difference from previous approaches is that this Ahead-of-Time Object Caching now works independently of the Garbage Collector used. This allows the optimization to be combined with different GC configurations. This can be particularly relevant in containerized environments. Applications start faster, auto-scaling reacts more efficiently, and resource requirements during the startup phase decrease.

With JEP 524, Java 26 improves the handling of cryptographic keys and certificates in its second preview. The JDK gains native support for PEM-encoded cryptographic objects.

PEM (“Privacy-Enhanced Mail”) is a text-based format for representing certificates and keys, used in many areas, such as for X.509 certificates, public and private keys, or Certificate Signing Requests. A typical PEM file looks like this:

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEi/kRGOL7wCPTN4KJ2ppeSt5UYB6u
cPjjuKDtFTXbguOIFDdZ65O/8HTUqS/sVzRF+dg7H3/tkQ/36KdtuADbwQ==
-----END PUBLIC KEY-----

Although this format is widely used in TLS and cloud environments, direct support in the JDK has been limited until now. Developers often had to rely on third-party libraries or implement their own parsers. The new API handles central tasks such as reading PEM data, removing headers and footers, and Base64 decoding. This allows certificates and keys to be processed directly using the JDK's built-in capabilities. PEM files frequently appear in current deployment scenarios, for example in Kubernetes secrets, TLS configurations, ACME/Let's Encrypt integrations, or mTLS setups between services. Native support reduces boilerplate code and decreases reliance on external security libraries.

The evolution of a platform includes not only adding new features but also consistently removing outdated technologies and correcting misbehavior. Java 26 contains two changes that appear unspectacular at first glance but indicate a clear strategic direction.

JEP 504 removes the Applet API permanently from the JDK. Applets were a central promise of the Java platform in the 1990s: Java code was supposed to run directly in the browser, enabling interactive web applications. That era is long gone. Modern browsers no longer support NPAPI plugins, security requirements have increased significantly, and web applications today are based on completely different architectures – such as JavaScript frameworks, REST APIs, or Single-Page Applications. The Applet API has already been marked as “deprecated for removal” for several Java versions and is now disappearing from the JDK permanently. This change has practically no impact on current applications.

JEP 500 is much more far-reaching: “Prepare to Make Final Mean Final.” In the Java language, the keyword final guarantees that a field cannot be changed after construction. In practice, however, this restriction could be circumvented via Reflection or low-level APIs like Unsafe. Many frameworks – especially serialization libraries – used this possibility to create objects without calling the constructor or to manipulate fields directly. However, this approach can violate important validation rules (invariants) of an object.

The following Java class with preconditions serves as an example:

public class Adult {

    private final Instant birthdate;

    public Adult(Instant birthdate) {
        this.birthdate = birthdate;
        if (isYoungerThan18(birthdate)) {
            throw new IllegalArgumentException("Not 18 yet");
        }
    }
}

If an object of this class is created via Reflection, for example, an application can set birthdate without the validation in the constructor being executed. The result is an object in an invalid state. JEP 500 therefore paves the way for a stricter integrity model. The goal is that final will truly guarantee immutability by default in the future. Unsafe access to such fields will be restricted, while transitional mechanisms via JVM parameters will continue to allow compatibility with existing code. This development is part of a larger trend under the keyword “Integrity by Default.” The platform is moving away from a model where frameworks can interfere arbitrarily deep with the object model towards an approach that better protects invariants and where the behavior of objects remains more reliable.

While the JEPs described so far mainly covered new topics, as with every JDK release, there are some long-running preview and incubator features. These include Primitive Types in Patterns (now in its fourth iteration), Structured Concurrency (sixth preview), and the Vector API (as an incubator feature for the eleventh time).

Since Java 21, Pattern Matching has been gradually developing into a central language concept. The goal is to express complex type checks and case distinctions more easily and safely. Together with Records and Sealed Classes, structures similar to algebraic data types can be modeled, whereby many invalid states can already be excluded at the type level. After Type Patterns, Pattern Matching in switch, Record Patterns, and Unnamed Patterns, Primitive Type Patterns are the next step in Java's development towards the data-oriented paradigm. The feature first appeared in Java 23 and appears in Java 26 with JEP 530 as the fourth preview. Little has changed content-wise compared to the last release – the OpenJDK team continues to gather feedback before finalizing the feature.

Pattern Matching allows comparing data structures with a pattern and simultaneously extracting parts of them. A pattern combines a condition (“does this type or value range match?”) with variables to which matching values are bound. While this concept has so far only applied to reference types, Java 26 extends it to primitive data types and allows primitive and reference types to be used interchangeably in the Pattern Matching context. The following example shows how to convert an int losslessly to a byte:

private static String checkByte(int value) {
    if (value instanceof byte b) {
        return "byte b = " + b;
    } else {
        return "kein byte: " + value;
    }
}

System.out.println(checkByte(127)); // byte b = 127
System.out.println(checkByte(128)); // kein byte: 128

In addition to simplified, lossless conversions, the current JEP primarily sharpens the dominance rules in [span class="tx_code">switch[/span] further. They ensure that no case is unreachable because a previous pattern already covers all possible values. The compiler now recognizes such situations more reliably and reports them as errors. Together with the already introduced pattern types, this is an important step towards a more consistent and declarative language. Developers can evaluate data structures directly, reduce boilerplate code, and formulate complex case distinctions more clearly. Further pattern types such as Array Patterns or the deconstruction of arbitrary classes (not just Records) are planned for future OpenJDK releases.

The introduction of Virtual Threads in Java 21 has significantly simplified concurrency in Java. However, these lightweight threads alone do not solve all the structural problems of parallel programs. How are multiple tasks orchestrated together? What happens if a subtask fails? And how do you ensure that no background tasks continue to run uncontrollably? This is precisely where the Structured Concurrency API comes in. The feature also first appeared as a preview in Java 21 and appears in Java 26 with JEP 525 in its sixth preview. After major API changes in Java 25, the current iteration primarily serves to gather further practical experience and stabilize the API design.

Classic Java applications often start parallel tasks via ExecutorService and Future:

ExecutorService executor = Executors.newFixedThreadPool(2);

Future<String> user = executor.submit(() -> loadUser());
Future<List<Order>> orders = executor.submit(() -> loadOrders());

String userResult = user.get();
List<Order> orderResult = orders.get();

This model has several disadvantages: error handling is fragmented, tasks can continue to run even though others have already failed, and the lifetime of tasks is not clearly tied to a scope. Structured Concurrency takes a different approach: parallel tasks are started and managed within a common lifecycle. Similar to try-with-resources, it clearly defines when tasks begin and when they are guaranteed to be finished:

try (var scope = StructuredTaskScope.open()) {
    Subtask<String> user = scope.fork(() -> loadUser());
    Subtask<List<Order>> orders = scope.fork(() -> loadOrders());
    scope.join();   // Join subtasks, propagating exceptions
    return new UserSummary(user.get(), orders.get());
}

All started tasks here belong to a common scope. If a task fails, the others are automatically canceled. After leaving the try block, it is guaranteed that no tasks are still running. An important part of the concept is centralized error handling. Instead of handling exceptions from individual tasks separately, join() aggregates the errors of the scope. If a task fails, join() throws a StructuredTaskScope.FailedException that encapsulates the original exception of the failed subtask. At the same time, remaining tasks are canceled.

So-called joiners define how the results of subtasks are combined and how errors or timeouts are handled. Commonly used strategies are already available as factory methods, such as Joiner.awaitAllSuccessfulOrThrow() (default joiner) or Joiner.anySuccessfulOrThrow(). You can also easily implement your own joiners. Java 26 also introduces the onTimeout() callback. This allows applications to decide how to handle timeouts – for example, by returning only the successful results obtained so far.

Structured Concurrency unfolds its full potential in combination with Virtual Threads. Each task can run in its own Virtual Thread without complex thread pool management. This enables a programming model that appears synchronous but scales highly in parallel internally. This approach is particularly suitable for typical patterns in backend architectures such as parallel service calls, data aggregation, or fan-out/fan-in structures. Structured Concurrency combines high scalability with a significantly more understandable programming model for concurrent applications. Complicated and error-prone alternatives such as reactive programming libraries or manual thread management become obsolete.

Another long-term performance project is continued in Java 26 with JEP 529: the Vector API. It is now appearing for the eleventh time in incubator status, making it one of the longest-running experimental features in recent years.

The goal of the Vector API is to make SIMD (Single Instruction, Multiple Data) instructions of current CPUs directly usable from Java. This allows multiple values to be processed in parallel with a single CPU instruction. This technique plays an important role in areas such as numerical simulations, image processing, signal processing, or machine learning algorithms. A simple comparison shows the difference. Without the Vector API, values are processed in a loop:

void scalarComputation(float[] a, float[] b, float[] c) {
    for (int i = 0; i < a.length; i++) {
        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
    }
}


Mit der Vector API lassen sich mehrere Werte gleichzeitig berechnen:

Listing 11: SIMD-Instruktionen mit Vector-API
```java
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;

void vectorComputation(float[] a, float[] b, float[] c) {
    int i = 0;
    int upperBound = SPECIES.loopBound(a.length);
    for (; i < upperBound; i += SPECIES.length()) {
        // FloatVector va, vb, vc;
        var va = FloatVector.fromArray(SPECIES, a, i);
        var vb = FloatVector.fromArray(SPECIES, b, i);
        var vc = va.mul(va)
                .add(vb.mul(vb))
                .neg();
        vc.intoArray(c, i);
    }
    for (; i < a.length; i++) {
        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
    }
}

The width of the operation depends on the CPU – for example, 128 or 256 bits. The JIT compiler can map these operations directly to the SIMD instructions of the respective hardware. The Vector API is strategically important for data-intensive applications, for example for vector calculations in databases, embedding comparisons in AI systems, or other numerical workloads. However, it still awaits an important foundation: Project Valhalla and the planned Value Types. Only with a more compact memory layout for values can the API unfold its full potential. Even though the Vector API remains in the incubator, its continuous development clearly shows the direction the platform is moving: Java should be able to efficiently utilize current hardware and remain competitive even for highly data- and compute-intensive applications like AI.

Java 26 is not a spectacular feature release, but an important puzzle piece in the long-term further development of the platform. Many changes appear small at first glance but prepare for larger developments. One of the most important ongoing projects is Project Valhalla, which fundamentally expands Java's type system with Value Types. Value Types promise more compact data structures without object headers, better cache locality, and less pressure on the garbage collector. This will bring significant performance gains, especially for numerical applications, data processing, or AI-related workloads. According to recent rumors, Value Classes could be integrated into the OpenJDK project as early as summer 2026 and appear as a preview feature with Java 28 in March 2027.

In parallel, the platform's security and integrity model is evolving. Under the guiding principle of “Integrity by Default,” there are restrictions on mechanisms that can circumvent object invariants via Reflection or unsafe APIs. The goal is a more stable and better optimizable object model. The innovations in Java 26 show a clear direction: the platform is systematically becoming more modern – from the language and concurrency model to the runtime and network protocols. In doing so, Java remains true to its proven approach of introducing major changes gradually and remaining as backward compatible as possible.

In addition to the ten JEPs presented, Java 26 includes numerous smaller improvements. Details can be found in the release notes. Changes to the Java class library can also be clearly traced in the Java Almanac, which lists the differences between JDK versions. And it's also worth looking ahead: the JEP index at Draft and submitted JEPs already contains numerous ideas for upcoming Java versions.

(mki)

Don't miss any news – follow us on Facebook, LinkedIn or Mastodon.

This article was originally published in German. It was translated with technical assistance and editorially reviewed before publication.