Java 21 Other Improvements

1. Introduction

Java 21 is a Long-Term Support (LTS) release, which means it is the version that most enterprises will upgrade to and run in production for years. While the headline features — virtual threads, pattern matching for switch, record patterns, and sequenced collections — get all the attention, Java 21 ships with a collection of smaller but significant improvements that affect performance, security, developer experience, and the language’s future direction.

Some of these features are fully finalized and production-ready. Others are in preview or incubator status, giving you a look at where Java is headed. This post covers everything that did not get its own dedicated tutorial: string templates, ZGC improvements, cryptography APIs, the Foreign Function and Memory API, simplified main methods, deprecations, and performance improvements.

Here is an overview of what we will cover:

Feature JEP Status in Java 21 Impact
String Templates JEP 430 Preview High — string interpolation at last
Generational ZGC JEP 439 Standard High — major GC performance improvement
Key Encapsulation Mechanism API JEP 452 Standard Medium — post-quantum crypto readiness
Foreign Function & Memory API JEP 442 Third Preview High — JNI replacement
Unnamed Classes & Instance Main JEP 445 Preview Medium — beginner-friendly Java
Deprecations and Removals Various Standard Medium — check your codebase
Performance Improvements Various Standard High — faster startup and throughput

2. String Templates (Preview)

String templates (JEP 430) bring string interpolation to Java. If you have used template literals in JavaScript (`Hello ${name}`), f-strings in Python (f"Hello {name}"), or string interpolation in Kotlin ("Hello $name"), you know how much cleaner embedded expressions make string construction. Java finally has its own version, and it is more powerful than any of those.

Important note: String templates were a preview feature in Java 21. They were subsequently removed in later JDK versions after community feedback. The concept may return in a different form in future Java releases. We cover them here for educational purposes and because they demonstrate Java’s evolving approach to string handling.

2.1 The STR Template Processor

The STR processor is the basic string template processor. It evaluates embedded expressions and produces a String:

// Before: String concatenation or String.format
String name = "Alice";
int age = 30;
String city = "Seattle";

// Old way 1: concatenation (messy with many variables)
String msg1 = "Hello, " + name + "! You are " + age + " years old and live in " + city + ".";

// Old way 2: String.format (positional, error-prone)
String msg2 = String.format("Hello, %s! You are %d years old and live in %s.", name, age, city);

// Old way 3: MessageFormat (verbose)
String msg3 = MessageFormat.format("Hello, {0}! You are {1} years old and live in {2}.", name, age, city);

// Java 21 Preview: String templates with STR processor
String msg4 = STR."Hello, \{name}! You are \{age} years old and live in \{city}.";
// Result: "Hello, Alice! You are 30 years old and live in Seattle."

2.2 Expressions Inside Templates

Template expressions are not limited to simple variables. You can embed any valid Java expression:

// Method calls
String greeting = STR."Hello, \{name.toUpperCase()}!";

// Arithmetic
String calc = STR."The sum of 5 and 3 is \{5 + 3}.";

// Ternary operator
boolean isAdmin = true;
String role = STR."User role: \{isAdmin ? "Administrator" : "Regular User"}";

// Array/collection access
String[] fruits = {"Apple", "Banana", "Cherry"};
String pick = STR."First fruit: \{fruits[0]}";

// Chained method calls
record User(String firstName, String lastName) {
    String fullName() { return firstName + " " + lastName; }
}
User user = new User("John", "Doe");
String info = STR."Welcome, \{user.fullName()}!";

// Multi-line string templates
String html = STR."""
    

\{user.fullName()}

Role: \{isAdmin ? "Admin" : "User"}

Registered: \{LocalDate.now()}

""";

2.3 The FMT Template Processor

The FMT processor works like STR but supports format specifiers similar to String.format():

// FMT supports format specifiers
double price = 49.99;
int quantity = 3;
double total = price * quantity;

String receipt = FMT."Item: %s\{name} | Qty: %d\{quantity} | Total: $%.2f\{total}";
// Result: "Item: Alice | Qty: 3 | Total: $149.97"

// Table formatting with alignment
record Product(String name, double price, int stock) {}
List products = List.of(
    new Product("Laptop", 999.99, 15),
    new Product("Mouse", 29.50, 200),
    new Product("Keyboard", 79.99, 85)
);

for (Product p : products) {
    System.out.println(FMT."%-15s\{p.name()} $%8.2f\{p.price()} %5d\{p.stock()} units");
}
// Output:
// Laptop           $ 999.99    15 units
// Mouse            $  29.50   200 units
// Keyboard         $  79.99    85 units

2.4 Safety Advantages

Unlike string interpolation in many other languages, Java’s string templates are processed through a template processor, which can validate and sanitize the embedded values. This design prevents injection attacks when building SQL queries, HTML, or JSON:

// The key insight: template processors can do more than just concatenation
// A hypothetical SQL processor could prevent SQL injection:
// String query = SQL."SELECT * FROM users WHERE name = \{userInput}";
// The SQL processor would automatically parameterize the query

// A hypothetical JSON processor could handle escaping:
// String json = JSON."""
//     {
//         "name": \{userName},
//         "bio": \{userBio}
//     }
//     """;
// The JSON processor would properly escape special characters

The template processor architecture is what sets Java’s approach apart from simple string interpolation. While STR and FMT just produce strings, the framework allows custom processors to produce any type — PreparedStatements, JSON objects, XML documents, or any validated output.

3. Generational ZGC

The Z Garbage Collector (ZGC) was introduced in Java 15 as a low-latency collector designed for applications that need consistently short pause times (under 1 millisecond). Java 21 enhances ZGC with generational support (JEP 439), which significantly improves its throughput and reduces memory overhead.

3.1 What Is Generational GC?

Most Java objects are short-lived. A typical request handler creates dozens of objects — strings, DTOs, temporary collections — that become garbage as soon as the request completes. The generational hypothesis says: most objects die young. Generational collectors exploit this by dividing the heap into a young generation (for newly created objects) and an old generation (for long-lived objects). Young generation collections are fast and frequent. Old generation collections are slow and rare.

Before Java 21, ZGC treated the entire heap as a single generation. It scanned everything during every collection cycle. This was simple but wasteful — why scan long-lived objects that are not going to be collected?

3.2 Performance Improvements

Generational ZGC delivers measurable improvements:

Metric Non-Generational ZGC Generational ZGC Improvement
Allocation throughput Baseline Higher Less memory pressure on the collector
Pause times Sub-millisecond Sub-millisecond Maintained (still ultra-low)
Memory overhead Higher Lower More efficient heap usage
CPU overhead Higher Lower Less scanning of long-lived objects
Application throughput Baseline 10-20% better in typical workloads Significant for high-allocation apps

3.3 How to Enable Generational ZGC

// Enable Generational ZGC (Java 21)
// JVM arguments:
// java -XX:+UseZGC -XX:+ZGenerational -jar myapp.jar

// In Java 23+, Generational ZGC becomes the default mode for ZGC:
// java -XX:+UseZGC -jar myapp.jar

// To use the OLD non-generational ZGC (Java 23+):
// java -XX:+UseZGC -XX:-ZGenerational -jar myapp.jar

// Typical production configuration
// java \
//   -XX:+UseZGC \
//   -XX:+ZGenerational \
//   -Xms4g \
//   -Xmx4g \
//   -XX:SoftMaxHeapSize=3g \
//   -jar myapp.jar

3.4 When to Use Generational ZGC

Generational ZGC is ideal for:

  • Latency-sensitive applications — microservices, real-time systems, trading platforms
  • Large heaps — applications with 8GB+ heap sizes where G1 pause times become noticeable
  • High allocation rates — applications that create many short-lived objects (web servers, stream processors)
  • Applications requiring consistent response times — SLA-bound services where 99th percentile latency matters

For most applications, Generational ZGC is a drop-in improvement over the non-generational version. There is no code change required — just update your JVM flags.

4. Key Encapsulation Mechanism API

Java 21 introduces a Key Encapsulation Mechanism (KEM) API (JEP 452). This is a cryptographic API that enables Java applications to use KEM-based key agreement schemes, which are a building block for post-quantum cryptography.

4.1 Why KEM Matters

Traditional key exchange algorithms like RSA and Diffie-Hellman rely on mathematical problems (integer factorization, discrete logarithms) that quantum computers could potentially solve efficiently. Post-quantum cryptography uses different mathematical foundations that are believed to resist quantum attacks. KEM is the mechanism that most post-quantum standards use for key exchange.

Think of it this way: when you send an encrypted message, you first need to agree on a shared secret key with the recipient. KEM provides a standardized way to do this that works with both classical and post-quantum algorithms.

4.2 How KEM Works

A KEM has three operations:

  1. Key pair generation — generate a public/private key pair
  2. Encapsulate — using the recipient’s public key, generate a shared secret and an encapsulated key (ciphertext)
  3. Decapsulate — using the private key, recover the shared secret from the encapsulated key
import javax.crypto.KEM;
import java.security.KeyPairGenerator;
import java.security.KeyPair;

public class KEMExample {

    public static void main(String[] args) throws Exception {

        // 1. Receiver generates a key pair
        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("X25519");
        KeyPair receiverKeyPair = keyPairGen.generateKeyPair();

        // 2. Sender encapsulates a shared secret using receiver's public key
        KEM kem = KEM.getInstance("DHKEM");
        KEM.Encapsulator encapsulator = kem.newEncapsulator(receiverKeyPair.getPublic());
        KEM.Encapsulated encapsulated = encapsulator.encapsulate();

        // The sender gets:
        // - A shared secret key (for encryption)
        byte[] senderSharedSecret = encapsulated.key().getEncoded();
        // - An encapsulation (ciphertext to send to receiver)
        byte[] encapsulation = encapsulated.encapsulation();
        // - Optional parameters
        byte[] params = encapsulated.params();

        // 3. Receiver decapsulates using their private key
        KEM.Decapsulator decapsulator = kem.newDecapsulator(receiverKeyPair.getPrivate());
        javax.crypto.SecretKey receiverSharedSecret = decapsulator.decapsulate(encapsulation);

        // Both sides now have the same shared secret
        System.out.println("Sender secret:   " + bytesToHex(senderSharedSecret));
        System.out.println("Receiver secret: " + bytesToHex(receiverSharedSecret.getEncoded()));
        // They match! This shared secret is used to derive encryption keys.
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}

4.3 Practical Impact

For most application developers, you will not use the KEM API directly. It is a building block used by:

  • TLS/SSL libraries — future versions of Java’s TLS implementation will use KEM for key exchange
  • Cryptographic library authors — Bouncy Castle, Google Tink, and other libraries will build on this API
  • Government and financial systems — organizations mandated to prepare for post-quantum threats

The important takeaway: Java 21 is laying the groundwork for post-quantum security. When quantum computers become practical, Java will be ready with standardized APIs that work with post-quantum algorithms. You do not need to change your code today, but you should know that Java is preparing.

5. Foreign Function & Memory API (Third Preview)

The Foreign Function & Memory (FFM) API (JEP 442) is a replacement for JNI (Java Native Interface). If you have ever worked with JNI, you know it is painful: you write C header files, compile native code, manage memory manually, and deal with cryptic crash dumps when something goes wrong. The FFM API makes calling native code from Java almost as easy as calling a Java method.

5.1 The Problem with JNI

JNI has been the only official way to call native (C/C++) code from Java since Java 1.1. But it has serious problems:

  • Complex — requires writing C code, generating headers with javac -h, and managing shared libraries
  • Unsafe — manual memory management, no protection from buffer overflows or use-after-free bugs
  • Brittle — native crashes take down the entire JVM with no useful error message
  • Slow — JNI call overhead is significant for frequently called functions

5.2 Calling C Functions from Java

With the FFM API, you can call a C standard library function directly from Java without writing any C code:

import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;

public class FFMExample {

    public static void main(String[] args) throws Throwable {

        // 1. Get a linker for the current platform
        Linker linker = Linker.nativeLinker();

        // 2. Look up the C standard library function "strlen"
        SymbolLookup stdlib = linker.defaultLookup();
        MemorySegment strlenAddr = stdlib.find("strlen").orElseThrow();

        // 3. Define the function signature: long strlen(char* s)
        FunctionDescriptor strlenDesc = FunctionDescriptor.of(
            ValueLayout.JAVA_LONG,    // return type: long (size_t)
            ValueLayout.ADDRESS       // parameter: pointer to char
        );

        // 4. Create a method handle for the function
        MethodHandle strlen = linker.downcallHandle(strlenAddr, strlenDesc);

        // 5. Allocate memory and call the function
        try (Arena arena = Arena.ofConfined()) {
            MemorySegment cString = arena.allocateFrom("Hello, Foreign Function API!");
            long length = (long) strlen.invoke(cString);
            System.out.println("String length: " + length); // 28
        }
        // Memory is automatically freed when the Arena closes
    }
}

5.3 Working with Native Memory

The FFM API provides safe, structured access to off-heap memory through MemorySegment and Arena:

import java.lang.foreign.*;

public class MemoryExample {

    public static void main(String[] args) {

        // Allocate native memory with automatic cleanup
        try (Arena arena = Arena.ofConfined()) {

            // Allocate a struct-like layout: { int x; int y; double value; }
            MemoryLayout pointLayout = MemoryLayout.structLayout(
                ValueLayout.JAVA_INT.withName("x"),
                ValueLayout.JAVA_INT.withName("y"),
                ValueLayout.JAVA_DOUBLE.withName("value")
            );

            // Allocate memory for the struct
            MemorySegment point = arena.allocate(pointLayout);

            // Write values using var handles
            var xHandle = pointLayout.varHandle(MemoryLayout.PathElement.groupElement("x"));
            var yHandle = pointLayout.varHandle(MemoryLayout.PathElement.groupElement("y"));
            var valueHandle = pointLayout.varHandle(MemoryLayout.PathElement.groupElement("value"));

            xHandle.set(point, 0L, 10);
            yHandle.set(point, 0L, 20);
            valueHandle.set(point, 0L, 3.14);

            // Read values back
            int x = (int) xHandle.get(point, 0L);
            int y = (int) yHandle.get(point, 0L);
            double value = (double) valueHandle.get(point, 0L);

            System.out.println("Point: (" + x + ", " + y + ") = " + value);
            // Output: Point: (10, 20) = 3.14

            // Allocate an array of 100 integers
            MemorySegment intArray = arena.allocate(ValueLayout.JAVA_INT, 100);
            for (int i = 0; i < 100; i++) {
                intArray.setAtIndex(ValueLayout.JAVA_INT, i, i * i);
            }
            System.out.println("Element 10: " + intArray.getAtIndex(ValueLayout.JAVA_INT, 10)); // 100

        } // All native memory freed here -- no leaks possible
    }
}

5.4 Comparison with JNI

Aspect JNI FFM API
Requires C code? Yes -- must write and compile native code No -- call native functions directly from Java
Memory management Manual (malloc/free) Automatic via Arena (try-with-resources)
Type safety Minimal -- pointer casting everywhere Strong -- MemoryLayout defines structure
Crash behavior JVM crash with no Java stack trace Java exception with meaningful error
Performance Higher overhead per call Lower overhead, optimized by JIT
Cross-platform Need to compile native code per platform Pure Java -- works everywhere

The FFM API is a game-changer for applications that need to interact with native libraries -- database drivers, GPU computing, operating system APIs, and any C/C++ library integration.

6. Unnamed Classes and Instance Main Methods (Preview)

Java has always been criticized for requiring too much ceremony to write a simple program. The classic "Hello World" in Java requires understanding classes, access modifiers, static methods, string arrays, and the special main method signature -- all before you print a single line. Java 21 addresses this with unnamed classes and instance main methods (JEP 445).

6.1 The Problem

// Traditional "Hello World" -- 5 concepts a beginner must understand:
public class HelloWorld {                             // 1. What is a class?
    public static void main(String[] args) {          // 2. What is public? static? void?
                                                      // 3. What is String[] args?
        System.out.println("Hello, World!");          // 4. What is System.out?
    }                                                 // 5. Why so many brackets?
}

6.2 The Simplified Solution

With unnamed classes and instance main methods, the same program becomes:

// Java 21 Preview: Simplified Hello World
void main() {
    System.out.println("Hello, World!");
}

// That's it. No class declaration, no public, no static, no String[] args.
// Save as HelloWorld.java and run: java --enable-preview HelloWorld.java

6.3 How It Works

The compiler wraps your code in an unnamed class automatically. The main method can be an instance method (not static) and does not need to accept String[] args. The launch protocol tries these signatures in order:

  1. static void main(String[] args) -- traditional
  2. static void main() -- no args
  3. void main(String[] args) -- instance method
  4. void main() -- simplest form
// You can define methods and fields alongside main
// Everything lives in an unnamed class

String greeting = "Hello";

String greet(String name) {
    return greeting + ", " + name + "!";
}

void main() {
    System.out.println(greet("Alice")); // Hello, Alice!
    System.out.println(greet("Bob"));   // Hello, Bob!
}

// You can even import packages
// import java.util.List;
//
// void main() {
//     var names = List.of("Alice", "Bob", "Charlie");
//     names.forEach(System.out::println);
// }

6.4 Teaching Benefits

This feature is primarily designed for:

  • Beginners -- learn I/O, variables, and control flow before classes and access modifiers
  • Scripting -- write quick utility programs without boilerplate
  • Prototyping -- test ideas quickly without creating a full project structure
  • Education -- instructors can teach concepts incrementally

For production code, you will still use properly named classes. But for learning, scripting, and quick experiments, this is a welcome reduction in ceremony.

7. Deprecations and Removals

Every Java LTS release deprecates or removes features that are outdated, insecure, or replaced by better alternatives. Java 21 continues this pattern. If you are migrating from Java 17 or earlier, check this list carefully.

7.1 Deprecated Features

Feature Status Replacement Action Required
Thread.stop() Degraded -- throws UnsupportedOperationException Use Thread.interrupt() and cooperative cancellation Rewrite thread cancellation logic
Windows 32-bit (x86) port Deprecated for removal Use 64-bit (x64) JDK Migrate to 64-bit
Dynamic loading of agents Warning issued (will be disabled by default later) Use -javaagent command-line flag Update agent loading to use command-line

7.2 Finalization Deprecation Progress

Object finalization (finalize() methods) has been deprecated since Java 9 and deprecated for removal since Java 18. Java 21 continues this trajectory:

// DEPRECATED -- do not use finalize() in new code
public class ResourceHolder {
    @Override
    @Deprecated(since = "9", forRemoval = true)
    protected void finalize() throws Throwable {
        // This is unreliable -- GC may never call it
        releaseNativeResource();
    }
}

// CORRECT -- use AutoCloseable and try-with-resources
public class ResourceHolder implements AutoCloseable {
    @Override
    public void close() {
        releaseNativeResource(); // deterministic cleanup
    }
}

// Or use Cleaner for reference-based cleanup
import java.lang.ref.Cleaner;

public class ResourceHolder implements AutoCloseable {
    private static final Cleaner CLEANER = Cleaner.create();
    private final Cleaner.Cleanable cleanable;

    public ResourceHolder() {
        long nativePtr = allocateNative();
        this.cleanable = CLEANER.register(this, () -> freeNative(nativePtr));
    }

    @Override
    public void close() {
        cleanable.clean(); // explicit cleanup
    }
}

7.3 Removed Features

Removed Was Deprecated Since Alternative
Applet API (java.applet) Java 9 No direct replacement -- use web technologies
Security Manager for authorization Java 17 Use container-level security, OS-level sandboxing
RMI Activation Java 15 Use modern RPC (gRPC, REST, message queues)

8. Performance Improvements

Java 21 includes several performance improvements beyond Generational ZGC that benefit all applications.

8.1 Virtual Thread Scheduler Improvements

The virtual thread scheduler in Java 21 is more efficient than the preview versions in Java 19-20:

  • Better work-stealing -- idle carrier threads steal work from busy ones more efficiently
  • Reduced pinning -- fewer scenarios where virtual threads pin their carrier thread
  • Improved synchronization -- monitors (synchronized blocks) interact better with virtual threads

8.2 Compiler Optimizations

The C2 JIT compiler includes improvements for:

  • Auto-vectorization -- more loop patterns are automatically converted to SIMD operations
  • Escape analysis -- better at detecting objects that do not escape a method scope, allowing stack allocation
  • Inlining decisions -- improved heuristics for when to inline method calls

8.3 Startup Time

Java 21 continues improvements to startup time through CDS (Class Data Sharing) enhancements:

// Generate a CDS archive for your application
// Step 1: Record class loading
// java -Xshare:off -XX:DumpLoadedClassList=classes.lst -jar myapp.jar

// Step 2: Create the archive
// java -Xshare:dump -XX:SharedClassListFile=classes.lst \
//       -XX:SharedArchiveFile=myapp.jsa -jar myapp.jar

// Step 3: Run with the archive (faster startup)
// java -Xshare:on -XX:SharedArchiveFile=myapp.jsa -jar myapp.jar

// Typical startup improvement: 20-40% reduction in startup time
// Especially impactful for microservices and serverless functions

8.4 Memory Footprint Reduction

Several internal changes reduce the memory footprint of Java applications:

  • Compact object headers (experimental) -- reduces object header size from 128 bits to 64 bits, saving 10-20% heap space for object-heavy applications
  • String deduplication improvements -- more aggressive deduplication of identical string values
  • Metaspace improvements -- better memory management for class metadata

8.5 Platform-Specific Optimizations

Platform Optimization Benefit
x86-64 Better use of AVX-512 instructions Faster mathematical operations and data processing
AArch64 (ARM) Improved code generation Better performance on Apple Silicon, AWS Graviton
All platforms Improved intrinsic methods Faster String, Math, and Array operations

9. Summary Table

Here is every improvement covered in this post with its status and a one-line description:

Feature JEP Status Description
String Templates 430 Preview (later removed) String interpolation with template processors for safe, readable string construction
Generational ZGC 439 Standard Adds generational collection to ZGC for improved throughput and reduced overhead
Key Encapsulation Mechanism API 452 Standard New cryptographic API for KEM-based key agreement, enabling post-quantum readiness
Foreign Function & Memory API 442 Third Preview Safe, efficient replacement for JNI to call native code and manage off-heap memory
Unnamed Classes & Instance Main 445 Preview Simplified program entry points -- write main() without class boilerplate
Thread.stop() Degraded -- Standard Thread.stop() now throws UnsupportedOperationException -- use interrupt() instead
Dynamic Agent Loading Warning 451 Standard Warns when agents are loaded dynamically; use -javaagent flag instead
Finalization Deprecation -- Deprecated for removal finalize() continues on the path to removal -- use Cleaner or AutoCloseable
Windows 32-bit Deprecation 449 Deprecated for removal Windows x86 (32-bit) port deprecated -- migrate to x64
Generational ZGC Performance 439 Standard 10-20% throughput improvement for high-allocation workloads
C2 Compiler Optimizations -- Standard Better auto-vectorization, escape analysis, and inlining decisions
CDS Enhancements -- Standard Improved Class Data Sharing for 20-40% faster startup times
Compact Object Headers (Experimental) -- Experimental Reduces object header size from 128 to 64 bits, saving 10-20% heap space
AArch64 Optimizations -- Standard Improved code generation for ARM platforms (Apple Silicon, Graviton)

Java 21 is more than its headline features. The improvements to ZGC, the cryptographic APIs preparing for the post-quantum era, the Foreign Function & Memory API replacing JNI, and the simplified entry points for beginners all contribute to making Java a more modern, performant, and accessible platform. If you are planning a migration from Java 17, these smaller improvements provide additional motivation beyond virtual threads and pattern matching.




Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *