Why Migrate to Java 21

Why Migrate to Java 21

Every mission-critical Java program needs to migrate to Java 21 as soon as possible. I'll explain why in this article.

When I asked Claude the same question, it gave me the following list of benefits:

  1. Virtual Threads (Project Loom) - Scale to millions of lightweight threads
  2. Foreign Function & Memory API (FFM) - Call native libraries without JNI
  3. Vector API (SIMD) - Hardware-accelerated parallel processing
  4. Pattern Matching for Switch - More expressive and concise code
  5. Record Patterns - Destructure records directly in patterns
  6. Sequenced Collections - Uniform API for ordered collections
  7. String Templates (Preview) - Safer string interpolation
  8. Generational ZGC - Improved garbage collection performance
  9. jextract Tool - Auto-generate Java bindings from C headers
  10. Long-Term Support (LTS) - Extended security updates and stability

Yes, instead of regular threads, you can now scale your async programming without much complexity. Even though Virtual Threads are the top feature in Java 21, they might not be as relevant to you if your current threads manage your workload pretty well. That's why, along with 100 other useful features and performance improvements, I want to highlight why one feature can be game-changing for your enterprise Java applications.


Project PANAMA

What is Project Panama?

Project Panama is an OpenJDK initiative launched to improve the connection between Java and non-Java APIs. Well, if that doesn't make sense to you, let's dive deep into understanding what non-Java APIs Java uses to make life easier for your applications.

The two main giant operating systems we know—Windows and Linux—are both primarily built on C and C++ at their core because these languages provide the performance, control, and hardware access that operating systems require. Let's move back to java to understand how java is interacting with these operating systems to handle native system calls.

To explain this I'll take a real world example that I faced recently. Let's assume your enterprise Java application needs to write an extensive number of files into the file system.

As we all know, Java is a platform-independent language that runs on the JVM. However, I/O operations (reading files, network communication, console input/output) require direct interaction with the operating system, which is platform-specific.

┌─────────────────────────────────────┐
│   Java Application Code             │
│   (Platform Independent)             │
└─────────────────────────────────────┘
              ↓ How to connect?
┌─────────────────────────────────────┐
│   Operating System                   │
│   (Windows, Linux, macOS)            │
│   (Platform Specific)                │
└─────────────────────────────────────┘        

Java code can't directly call OS functions because:

  • Each OS has different APIs (Win32 vs POSIX)
  • OS functions are written in C/C++
  • Java runs in the JVM, isolated from native code

That's why Java uses the Java Native Interface (JNI) as the bridge. That's where all the magic happens, at least before Java 21.


The JNI Bridge Architecture

Java Layer (Your Code)
    ↓
Java Standard Library (java.io, java.net)
    ↓
JNI Layer (native keyword methods)
    ↓
Native C/C++ Implementation
    ↓
Operating System Calls
    ↓
Hardware (Disk, Network Card, etc.)        

Traditional JNI Overhead

Every I/O operation involves:

1. Java method call
2. JNI boundary crossing (expensive!)
3. Parameter marshalling (convert Java types to C types)
4. Native C function execution
5. OS system call
6. Return value marshalling (convert C types back to Java)
7. JNI boundary crossing (expensive!)
8. Return to Java        

The JNI boundary crossing is slow because:

  • Context switching between JVM and native code
  • Type conversion overhead
  • Safety checks
  • Cannot be optimized by JIT compiler


Since our example scenario is related to Java File I/O, I'm specifically talking about the File I/O related things only, but the principal remains same for all other Java I/O retaliated activities. I'm certain that after explaining JNI, you'll understand where I'm going with this explanation. So let's deep dive into the real world problem that we need to fix.

Problem: Need to improve the file generation time significantly.

After a few attempts at logic refactoring and spending weeks, I managed to improve the file generation time by only 2%–3%. Then I felt that I was going in the wrong direction. That's where things started to change. While deep diving into the debug logs, I was able to understand where the actual latency was coming from. IIt's not the logic, and it's not the JVM memory. The latency is coming from the JNI layer while wrapping the Java code into C++ to write the files.

Now it's pretty clear that, to see any significant improvement, I need to change the JNI layer of Java. While researching that, I found an interesting topic: Project Panama. Lucky me, because Project Panama is now officially part of Java 21.

Foreign Function & Memory API (FFM) - Call native libraries without JNI

The FFM API Solution

With Java 21's Foreign Function & Memory API you can simply call Operating system functionality without any wrapper classes:

import java.lang.foreign.*;

public class ModernIO {
    private static final Linker LINKER = Linker.nativeLinker();
    private static final SymbolLookup STDLIB = LINKER.defaultLookup();
    
    public static void readFileDirect(String path) throws Throwable {
        // Find the open() function
        MemorySegment openFunc = STDLIB.find("open").orElseThrow();
        
        // Define function signature
        FunctionDescriptor openDesc = FunctionDescriptor.of(
            ValueLayout.JAVA_INT,     // returns int (file descriptor)
            ValueLayout.ADDRESS,      // const char *pathname
            ValueLayout.JAVA_INT      // int flags
        );
        
        // Create method handle
        MethodHandle open = LINKER.downcallHandle(openFunc, openDesc);
        
        try (Arena arena = Arena.ofConfined()) {
            // Allocate native string
            MemorySegment pathStr = arena.allocateUtf8String(path);
            
            // Call open() directly - NO JNI!
            int fd = (int) open.invoke(pathStr, 0); // O_RDONLY = 0
            
            if (fd >= 0) {
                System.out.println("File opened with fd: " + fd);
                
                // Similarly can call read() and close() directly
                // 5-10x faster than traditional JNI!
            }
        }
    }
}        



Performance Comparison

Article content

Why This Matters

  • Understanding: You now know what happens "under the hood" when you do I/O
  • Java 21: Project Panama eliminates JNI overhead for better performance
  • Architecture: Shows how Java maintains platform independence while accessing OS features

Key Takeaway

Every time you do I/O in Java (files, network, console), you're making native system calls through JNI, whether you realize it or not!

Java 21's FFM API is revolutionizing this by providing a faster, safer, and more modern way to interact with native code and perform I/O operations.

I hope you learn new things from this article. Thanks for reading and happy coding! Cheers!

To view or add a comment, sign in

More articles by Naween Bhanuka

Others also viewed

Explore content categories