Skip to content
Published on

Modern Java 2026 — Java 25 LTS / Spring Boot 3.5 / Quarkus / Virtual Threads / GraalVM 25 / Loom / Panama Deep Dive

Authors

Modern Java 2026 — Java 25 LTS / Spring Boot 3.5 / Quarkus Deep Dive

In 2026 Java is no longer the "conservative enterprise language" it used to be. Java 21 LTS (Sept 2023) made virtual threads GA; Java 25 LTS (Sept 2025) finalized pattern matching and effectively made virtual threads the default execution model. GraalVM 25 native image is now a first-class citizen in Spring Boot 3.5, and Quarkus 3.20 delivers supersonic, subatomic boot times on Lambda and Kubernetes. At the same time, large-traffic services like Toss, Kakao Pay, Mercari, LINE, NTT Data, and Rakuten still run their cores on Java/Kotlin/Spring. This article is a single-sitting tour of the entire modern Java ecosystem, as of May 2026.


1. Modern Java in 2026 — what Java 21 LTS changed, what Java 25 LTS finished

A quick timeline:

  • Java 17 LTS (Sept 2021) — Records, sealed classes, pattern matching for instanceof settled. The baseline for many enterprise codebases.
  • Java 21 LTS (Sept 2023) — Virtual Threads GA, Record Patterns, Pattern Matching for switch GA, Sequenced Collections, Generational ZGC (preview). The watershed for modern Java.
  • Java 22 (Mar 2024) — Project Panama Foreign Function & Memory API (FFM) GA, Unnamed Variables and Patterns, String Templates (preview).
  • Java 23 (Sept 2024) — Markdown JavaDoc, Primitive Types in Patterns (preview), ZGC default mode switched to Generational.
  • Java 24 (Mar 2025) — Compact Object Headers (preview), Stream Gatherers, JEP 491 and other incremental improvements.
  • Java 25 LTS (Sept 2025)Virtual threads as the default recommendation (JEP), Module Import Declarations (preview), Pattern Matching for switch finalized, JEP 470 PEM keystore, Generational ZGC default, Compact Object Headers default.

Two takeaways. (1) Any new Java service started in 2026 is on Java 21 or Java 25 LTS; Java 17 is now in maintenance mode. (2) Concurrency is no longer a thread-pool-tuning problem — you can write blocking-style code and let the JVM handle it. The "callback hell" era of reactive code is over.


2. Java 25 LTS (Sept 2025) — virtual threads default, pattern matching, module imports

The three biggest changes in Java 25 LTS:

2.1 Virtual Threads are effectively the default

Virtual threads went GA in Java 21. With Java 25 LTS, JEP 491 (Synchronize Virtual Threads without Pinning) and friends are stabilized, making virtual threads the default for all new code. The core API:

// Java 25 — simplest possible virtual thread usage
Thread.startVirtualThread(() -> {
    var response = httpClient.send(request, BodyHandlers.ofString());
    log.info("got {}", response.statusCode());
});

// Executor style
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i ->
        executor.submit(() -> fetchUser(i))
    );
} // auto-join

You can create 10,000 concurrent HTTP calls and only end up with a few dozen OS threads. The JVM cooperatively mounts/unmounts virtual threads onto carrier threads (the ForkJoinPool).

2.2 Pattern Matching for switch (finalized)

switch becomes a true data-driven dispatch.

sealed interface Shape permits Circle, Rect, Triangle {}
record Circle(double r) implements Shape {}
record Rect(double w, double h) implements Shape {}
record Triangle(double b, double h) implements Shape {}

double area(Shape s) {
    return switch (s) {
        case Circle(double r)         -> Math.PI * r * r;
        case Rect(double w, double h) -> w * h;
        case Triangle(double b, double h) -> 0.5 * b * h;
    };
}

sealed interface plus record deconstruction plus exhaustiveness checking arrive as a package — Scala/Kotlin/Rust developers will recognize this immediately. The big win is that no default branch is needed.

2.3 Module Import Declarations (preview)

Collapse the boilerplate import java.util.*; import java.util.concurrent.*; stack at the top of every file into a single line.

import module java.base;
import module java.net.http;

void main() {
    var client = HttpClient.newHttpClient();
    var req = HttpRequest.newBuilder(URI.create("https://example.com")).build();
}

Combined with JEP 477 (Implicit Classes / Instance Main), small scripts get down to almost Python length. Great for teaching and for jbang.


3. Project Loom — virtual threads + structured concurrency in practice

The real value of Loom is that virtual threads and structured concurrency arrived as one package.

3.1 How virtual threads actually work

A virtual thread is not an OS thread — it is a Java object on the heap. The JVM mounts it on a carrier (a platform thread) and unmounts it at blocking points like I/O, then mounts another virtual thread in its place. Consequences:

  • HTTP calls, JDBC, file I/O — just write synchronous, blocking code. No more CompletableFuture chains or Mono/Flux plumbing.
  • The historical "synchronized pins the carrier" problem was fixed in Java 24 (JEP 491). On Java 25 LTS, synchronized is generally safe.
  • Never pool virtual threads. Pooling is the anti-pattern here — create a new one per task.

3.2 Structured Concurrency (JEP 480 / 505)

Bundle multiple concurrent tasks into a parent/child relationship. If a child fails, the siblings get cancelled too.

import jdk.incubator.concurrent.StructuredTaskScope;

record UserDashboard(User user, List<Order> orders, List<Notice> notices) {}

UserDashboard build(long userId) throws Exception {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        var userF    = scope.fork(() -> userService.findById(userId));
        var ordersF  = scope.fork(() -> orderService.findByUser(userId));
        var noticesF = scope.fork(() -> noticeService.findByUser(userId));

        scope.join();          // wait for all
        scope.throwIfFailed(); // propagate any failure

        return new UserDashboard(userF.get(), ordersF.get(), noticesF.get());
    }
}

The three tasks genuinely share the parent's lifecycle. Interrupt the parent and the children die with it; stack traces preserve the parent/child relationship. In practice, "one request equals one scope" becomes the default pattern.

3.3 Scoped Values (JEP 506)

A virtual-thread-friendly replacement for ThreadLocal.

private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

void handle(HttpRequest req) {
    ScopedValue.where(REQUEST_ID, req.header("x-request-id"))
        .run(() -> business());
}

void logInfo() {
    log.info("request-id={}", REQUEST_ID.get());
}

Unlike ThreadLocal, scoped values are (1) immutable, (2) automatically released when their scope ends, and (3) nearly free to copy across virtual threads. Once you commit to virtual threads, gradually shrink your ThreadLocal usage.


4. Spring Boot 3.5 — Native + GraalVM Integration

Spring Boot 3.5 (GA May 2025) targets Java 17 minimum, Java 21/25 recommended, built on Jakarta EE 11. Key points:

4.1 Virtual threads with one line

# application.properties
spring.threads.virtual.enabled=true

That single line makes Tomcat / Jetty / Undertow run request handlers on virtual threads. You can also switch the default @Async pool over:

@Configuration
class AsyncConfig implements AsyncConfigurer {
    @Override public Executor getAsyncExecutor() {
        return Executors.newVirtualThreadPerTaskExecutor();
    }
}

HikariCP, Reactor Netty clients, and external HTTP calls all work unchanged. Java 25 LTS is recommended if you depend on libraries heavy in synchronized.

4.2 GraalVM Native Image as a first-class story

# Spring Boot + GraalVM native image
./mvnw -Pnative native:compile

# Or via cloud-native buildpack
./mvnw spring-boot:build-image -Pnative
  • Boot time around 50 to 100 ms (vs 1 to 3 seconds on the JVM).
  • Memory usage roughly one-third to one-fifth of the JVM equivalent.
  • Caveats: AOT compilation is slow, and reflection / proxy-heavy libraries need hints.

Spring Boot 3.5 has matured the hint-emitting story — annotations like @RegisterReflectionForBinding are stable, and most Spring Data / Jackson / JPA code compiles to native with no manual hints.

4.3 Observability as a first-class concern

Micrometer plus Tracing (Brave / OpenTelemetry) is a default dependency. You can emit spans with the @Observed annotation alone — no OTel Java agent required.

@Service
class OrderService {
    @Observed(name = "order.create")
    public Order create(OrderRequest req) { /* ... */ }
}

To export to an OTel collector, just add management.otlp.tracing.endpoint.


5. Quarkus 3.20 — Supersonic Subatomic Java

Quarkus (Red Hat) positions itself as Kubernetes-native Java. Differentiators:

5.1 Boot time / memory advantage

  • JVM mode: 1 to 2 seconds boot, 100 to 200 MB RSS.
  • Native mode (GraalVM): 20 to 30 ms boot, 30 to 50 MB RSS.

Those numbers map well onto Knative / Lambda / Cloud Run — scale-to-zero actually pays off.

5.2 Developer experience — Dev Mode + Continuous Testing

./mvnw quarkus:dev
  • Save a file and it hot-reloads instantly.
  • Press r to re-run only the tests affected by your change (continuous testing).
  • Built-in Dev UI at http://localhost:8080/q/dev/ for inspecting extensions, running DB queries, hitting endpoints.

5.3 Code example — REST + Panache + Reactive

@Path("/users")
public class UserResource {

    @GET @Path("/{id}")
    public Uni<User> get(@PathParam("id") Long id) {
        return User.<User>findById(id);
    }

    @POST
    public Uni<Response> create(User user) {
        return Panache.withTransaction(user::persist)
            .replaceWith(Response.created(URI.create("/users/" + user.id)).build());
    }
}

Uni/Multi from SmallRye Mutiny are the default reactive types. That said, in Quarkus 3.x it has become normal to skip Hibernate Reactive entirely and use plain blocking code on top of virtual threads.

5.4 Extension ecosystem

There are over 600 quarkus- extensions. Kafka, gRPC, Redis, Vault, Keycloak, Kubernetes config, OpenAPI, the full MicroProfile spec — almost everything you would want is first-class.


6. Micronaut 4 — Reactor friendly

The Micronaut 4 differentiator is compile-time DI. Bean metadata is generated by annotation processors instead of via reflection, which is exactly what GraalVM native image likes.

@Controller("/hello")
public class HelloController {
    @Get(uri = "/{name}", produces = MediaType.TEXT_PLAIN)
    public String hello(String name) {
        return "Hello, " + name;
    }
}
  • Works naturally with Project Reactor. Easy migration for Spring WebFlux users.
  • GraalVM native image is the default scenario. Compilation is faster than Spring's.
  • AWS Lambda / GCP Cloud Functions / Azure Functions adapters are first-class.

Spring still dominates at companies like Toss and Kakao, but Micronaut has appeared in a handful of new services where cold start is critical.


7. Helidon 4 / Vert.x 5 — the other options

7.1 Helidon 4 (Oracle)

Oracle's microservices framework. Helidon Nima is a new web engine designed from day one to run on virtual threads — no Netty.

public class Main {
    public static void main(String[] args) {
        WebServer.builder()
            .routing(r -> r.get("/hello", (req, res) -> res.send("Hello")))
            .build()
            .start();
    }
}

Because Nima uses virtual threads directly as carriers, the concurrency model is the simplest of any framework here, and JVM memory usage is very low. A natural fit for Oracle Cloud.

7.2 Vert.x 5

An event-loop-based reactive toolkit. Best when you need extremely high concurrent connection counts on a single node.

Vertx vertx = Vertx.vertx();
vertx.createHttpServer()
    .requestHandler(req -> req.response().end("Hello"))
    .listen(8080);

Vert.x 5 also supports virtual-thread mode, which sidesteps the callback hell. Common workload: message gateways, IoT gateways, WebSocket servers — anything where connection counts explode.


8. ZGC — sub-ms pause GC

As of Java 25 LTS, Generational ZGC is the default garbage collector.

8.1 Why it matters

G1 GC can produce stop-the-world pauses of tens to hundreds of milliseconds. ZGC keeps pauses under 1 ms, with the same characteristics whether the heap is a few GB or hundreds of GB.

8.2 How to enable

java -XX:+UseZGC -Xmx16g -jar app.jar

From Java 21 you had to flip on generational mode with -XX:+ZGenerational. From Java 25 LTS that is the default.

8.3 Trade-offs

  • Throughput is slightly lower than G1 (typically 5 to 10 percent).
  • Memory footprint is larger than G1 (color pointer overhead).
  • The win is on p99 latency workloads: payments, trading, ad bidding.

For Toss payment gateways or Mercari order processing, where p99 is an SLA, ZGC is effectively the standard.


9. Project Valhalla — value types

Valhalla still lives mostly in the "almost there" land, but as of 2026 value types (JEP 401 / 402) are at preview.

9.1 What changes

value class Point {
    int x;
    int y;
}
  • Point has no identity== becomes value equality.
  • The JVM can flatten value types into arrays; a Point[] becomes a genuinely contiguous block of memory.
  • Boxing goes away. Value types behave like primitives but carry a real class interface.

9.2 Why it matters

Until now, every Java object lived behind a pointer on the heap. For numeric computing, ML tensors, or game engines — workloads with hundreds of thousands of small objects — boxing and cache misses cost a lot relative to C++ or Rust. Once Valhalla is GA, much of that gap closes. Production adoption in 2026 is still premature, but libraries like DJL are already preparing.


10. Project Panama (FFI, Java 22 GA) — with jextract

Panama's Foreign Function & Memory API (FFM) went GA in Java 22. The successor to JNI, and a far safer one.

10.1 Basic use — calling a C function

import java.lang.foreign.*;
import static java.lang.foreign.ValueLayout.*;

try (Arena arena = Arena.ofConfined()) {
    Linker linker = Linker.nativeLinker();
    SymbolLookup stdlib = linker.defaultLookup();

    MethodHandle strlen = linker.downcallHandle(
        stdlib.find("strlen").orElseThrow(),
        FunctionDescriptor.of(JAVA_LONG, ADDRESS)
    );

    MemorySegment cString = arena.allocateUtf8String("Hello, Panama!");
    long len = (long) strlen.invoke(cString);
    System.out.println(len); // 14
}

Wins over JNI:

  • Arena manages memory lifecycle — leaks are nearly impossible.
  • Safe off-heap memory access, with bounds checking.
  • Auto-generated bindings via jextract eliminate most boilerplate.

10.2 jextract — generate Java bindings from C headers

# Example: generate Java bindings from SQLite's C header
jextract --source --output gen \
  -t org.sqlite.ffi \
  /usr/include/sqlite3.h

You get a class like org.sqlite.ffi.sqlite3_h whose sqlite3_open/sqlite3_exec are exposed as Java MethodHandle values. The result: you can call native libraries without writing any JNI.

10.3 Where it shows up

  • High-performance compression (zstd, snappy) libraries.
  • ML inference integrations (ONNX Runtime, llama.cpp).
  • Hardware acceleration calls (CUDA, Apple Metal).
  • OLAP engines that need to mmap and crunch files (think Apache Arrow).

In the JNI era you used native code reluctantly, only when forced by performance. In the Panama era it is the default option.


11. Project Babylon — JVM GPU/AI compute

Babylon (2024 onwards) is an OpenJDK project. Through a feature called Code Reflection, it lets you translate Java code into other programming models (GPU, SQL, differentiable functions, ...).

11.1 Motivation

Until now, using a GPU from Java meant (1) calling CUDA via JNI, or (2) reaching for external tools like ONNX Runtime or TornadoVM. Babylon aims to provide a standard API that lets you compile pure Java code into a GPU kernel.

11.2 The picture

Java source           Reflected method body (IR)        Target backend
@CodeReflection  ────►  Op tree (SSA)             ────►  SPIR-V / PTX / SQL / ...
void matmul(...) { }

Babylon is infrastructure for library authors to build their own annotations plus IR consumers. TornadoVM and HAT (Heterogeneous Accelerator Toolkit) sit on top of Babylon, for example.

11.3 What it means in 2026

Java has cemented its role as the serving-layer language of AI/ML, but on the training/inference side it has always lagged Python. Once Babylon stabilizes, writing GPU kernels in Java becomes practical, opening a path to making parts of the inference service truly native. It is still incubating, but the direction is right.


12. GraalVM 25 — native image

GraalVM 25 (Sept 2025) shipped together with OpenJDK 25. Two pillars:

12.1 Native Image

# Works with Spring Boot / Quarkus / Micronaut alike
native-image --no-fallback -jar app.jar
  • Boot: 20 to 100 ms.
  • Memory: one-third to one-fifth of the JVM.
  • Trade-offs: builds take minutes; reflection / proxies / resource loading must be explicit.

The Spring Boot 3.5 + GraalVM 25 combo auto-collects reachability metadata, so most standard Spring code compiles native without manual hints.

12.2 Polyglot

GraalVM also runs JavaScript / Python / Ruby / Wasm / R / LLVM bitcode side by side. You can invoke a JS function from Java directly:

try (Context ctx = Context.create("js")) {
    Value array = ctx.eval("js", "[1,2,3]");
    System.out.println(array.getArraySize()); // 3
}

In practice, native image is used much more often than polyglot.

12.3 Oracle GraalVM vs GraalVM CE

From Java 21 onward, Oracle GraalVM has been released under the GFTC license (free for production use, with the fast-path compiler included). The CE/EE split has effectively dissolved — Oracle GraalVM is free for production.


13. Maven 4 + Gradle 9 + jbang

13.1 Maven 4

The first major Maven upgrade in a decade. Highlights:

  • Consumer/build POM split — published POMs are clean.
  • Automatic dependency inference for multi-module builds.
  • Stronger build cache and parallel build support.
  • New tooling like mvnup / mvnsh.

Existing pom.xml files mostly work as-is; the migration cost is low.

13.2 Gradle 9

  • Configuration Cache is stable — gradle build starts almost instantly on a cache hit.
  • Kotlin DSL is the recommended default; Groovy DSL still works but new projects should pick Kotlin.
  • Build authoring guidance is standardized (buildSrc to version catalog).
plugins {
    id("org.springframework.boot") version "3.5.0"
    id("io.spring.dependency-management") version "1.1.6"
    kotlin("jvm") version "2.1.0"
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    runtimeOnly("org.postgresql:postgresql")
}

13.3 jbang — Java as scripts

# Run a single-file Spring Boot app
jbang hello.java
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS info.picocli:picocli:4.7.6

import picocli.CommandLine;
import picocli.CommandLine.Command;

@Command(name = "hello")
public class hello implements Runnable {
    public void run() { System.out.println("Hello from jbang!"); }
    public static void main(String[] args) { new CommandLine(new hello()).execute(args); }
}

//DEPS declares dependencies, and Maven's resolver fetches them. Great for CLI tools, CI automation, ad-hoc data crunching. Combine with Java 25's Implicit Classes (JEP 477) and you get something almost as concise as Python.


14. Korea / Japan — Toss, Kakao Pay, Mercari, LINE, NTT Data, Rakuten

14.1 Korea

  • Toss — Core payment/transfer systems are still mostly Java + Spring Boot, with newer services in Kotlin/Spring. Virtual threads adoption started showing up in 2024 talks. ZGC is the standard GC for payment gateways.
  • Kakao Pay — Java + Spring Boot at the core, Kafka-based event-driven architecture, virtual threads being introduced gradually alongside reactive.
  • NAVER / LINE — Many services run on Java/Kotlin/Spring. LINE Pay runs multi-region in Korea, Japan, and Taiwan.
  • Coupang — Java + Spring + Kafka at the core, with Kotlin gradually growing in newer services.

14.2 Japan

  • Mercari — Search and recommendations lean on Go and Python, but the product, payment, and settlement stacks are Java/Kotlin. Famous for sharing detailed JVM tuning notes, including ZGC.
  • LINE Yahoo — Large-scale Java + Kotlin + Spring deployment. Some message gateways run on Vert.x.
  • NTT Data — The enterprise SI standard. Java 17/21 LTS is entrenched in mission-critical systems. Spring Boot is the default, with Helidon and Quarkus also adopted in places.
  • Rakuten — The global e-commerce platform is mostly Java + Spring. Rakuten Mobile uses Vert.x in some reactive paths.
  • Denso / Sony / Hitachi — Helidon Nima is gaining traction in embedded and IoT gateway scenarios.

The common pattern: "Java/Kotlin/Spring monolith + Kafka event-driven + selective virtual threads / native image in newer services."


15. Who should pick Java/Spring — enterprise / finance / monoliths / polyglot teams

  • Enterprise / finance / telecom — Regulation, audit trails, and long-term maintainability are paramount. Java 25 LTS's 8-year support window, the Spring ecosystem, and Quarkus operational tooling are hard for other languages to match.
  • New services that start as a monolith — Spring Boot's "convention over configuration" gets you to value faster than almost anything else. Split into modules later, not first.
  • Teams sitting on big JVM library assets — Kafka, Hadoop, Spark, Flink, Cassandra, Elasticsearch — almost the entire data infrastructure stack is JVM. Operate, integrate, and debug in one language.
  • Serverless where cold start matters — Quarkus + GraalVM or Micronaut + GraalVM. Plain Java/Spring is weak on cold start.
  • High connection counts (IoT, WebSocket, game servers) — Vert.x or Helidon Nima is the better fit.
  • Data science / ML training pipelines — Python still wins. Java tends to be the serving / inference gateway.
  • CLI single-binary tools — Go or Rust is more natural. (Caveat: jbang + GraalVM is surprisingly competitive.)
  • Systems programming — Rust / C++ / Zig.

15.4 Java vs Kotlin

Same JVM, different selection criteria. Java when "the organization standard is Java" or "the library is Java-first"; Kotlin when "the domain wants DSLs" or "code is shared with Android." Spring supports Kotlin as a first-class citizen, so Korean and Japanese companies are rapidly choosing Kotlin for new services.


References