Skip to content

GraalVM and Modern JVM

GraalVM is a high-performance JDK distribution that extends the standard HotSpot JVM with an Advanced just-in-time compiler (Graal), a native image generator, and polyglot execution Capabilities. It compiles Java applications ahead-of-time into standalone native executables, Dramatically reducing startup time and memory footprint compared to the traditional JVM. The foreign Function and memory API (Project Panama) provides a modern, safe alternative to JNI for calling Native code and managing off-heap memory.

These technologies matter because they address the three persistent complaints about Java: slow Startup, high memory consumption, and painful native interop. GraalVM native images bring Java into Territory previously reserved for Go and Rust — sub-millisecond startup and single-digit MB RSS for Microservices and CLI tools. The Foreign Function and Memory API replaces the brittle, error-prone JNI mechanism with a type-safe, allocation-tracking API that is practically usable.

ComponentPurposeStatus
Graal JIT CompilerReplaces C2 as an advanced JIT compilerProduction (JDK 17+)
Native ImageAOT compilation to standalone executablesProduction (JDK 22+)
Polyglot RuntimeRun JavaScript, Python, Ruby, R, LLVM alongside JavaCommunity Edition
Truffle FrameworkAPI for building high-performance language runtimesProduction
VisualVM IntegrationProfiling and diagnostics for Graal-compiled codeProduction
## The Graal Compiler

HotSpot has historically used two JIT compilers: C1 (client compiler, fast compilation, less Optimization) and C2 (server compiler, slower compilation, aggressive optimization). The tiered Compilation strategy starts with the interpreter, promotes hot methods through C1, and eventually Compiles with C2 for maximum throughput.

Graal is a replacement for C2 written in Java. It is itself a Java program that compiles Java Bytecode to machine code. Being written in Java means Graal benefits from the same JVM optimizations It produces, and its codebase is far more approachable for contributors than C2 (which is written in C++).

Definition. The Graal compiler is a graph-based JIT compiler implemented in Java that replaces C2 as the top-tier optimizing compiler. It operates on a sea-of-nodes intermediate representation (IR) that is more expressive than C2”s ideal graph.

FeatureC2Graal
ImplementationC++Java
IRIdeal graph (sea-of-nodes)Sea-of-nodes (Graal IR)
Inlining heuristicsFixed thresholdsProfile-guided, more aggressive
Escape analysisBasicAdvanced (partial escape analysis)
Loop optimizationsLimitedExtensive (loop peeling, unrolling)
DeoptimizationSpeculativeSpeculative with better recovery
ExtensibilityHard (C++ plugin system)Easy (Java plugin system)
Interpreter -> C1 (quick) -> C1 (detailed) -> C2 (or Graal) -> Native code

When Graal is enabled, it replaces C2 as the top tier. Enable it with:

Terminal window
## Enable Graal as the top-tier JIT compiler (requires GraalVM or JDK with Graal)
java -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler -jar app.jar

On GraalVM, this is the default. On standard OpenJDK, you must explicitly enable it.

Graal can use profiling information from previous runs to guide compilation decisions. This is Particularly impactful for native image builds:

Terminal window
## Step 1: Run the application with profiling enabled
java -agentlib:native-image-agent=config-output-dir=META-INF/native-image \
-jar app.jar
# Step 2: Build the native image using the collected profiles
native-image -H:ConfigurationFileDirectories=META-INF/native-image \
-jar app.jar

The agent records which classes are instantiated, which methods are called, which reflection sites Are used, and which resources are loaded. This information feeds the closed-world analysis during Native image generation, producing a smaller and faster binary.

Graal outperforms C2 on workloads involving:

  • Frequent object allocation and escape analysis: Graal’s partial escape analysis can scalar-replace objects that escape only on some paths, where C2 would give up entirely.
  • Complex control flow: Graal’s IR represents control flow more precisely, enabling optimizations that C2 misses.
  • Polymorphic call sites: Graal makes better inlining decisions based on profile data.
  • Loop-heavy numeric code: Graal’s loop optimizations (loop peeling, loop unrolling, loop invariant code motion) are more comprehensive.

Definition. Native Image performs ahead-of-time (AOT) compilation of a Java application into a Standalone native executable using closed-world static analysis. It operates on the SubstrateVM, a Minimal JVM runtime that replaces HotSpot entirely.

The closed-world analysis works as follows:

  1. Reachability analysis: Starting from entry points (main method, JNI methods), the analysis traces all reachable classes, methods, and fields. Anything not reachable is discarded.
  2. Points-to analysis: Determines which concrete types each reference may hold at each program point. This drives devirtualization — turning virtual calls into direct calls.
  3. Heap snapshot: Objects allocated at build time are serialized into the image heap. This includes static final fields, interned strings, and classes configured for build-time initialization.
  4. Code compilation: All reachable methods are compiled to native machine code. No bytecode is included in the output.

The result is a native executable with no JVM dependency, no class loading at runtime, and no JIT Compilation overhead.

Terminal window
# Using the native-image command directly
native-image --no-fallback -H:Name=myapp -cp target/classes com.example.Main
# Using the Maven plugin
# pom.xml configuration:
<plugin>
<groupId>org.graalvm.nativeimage</groupId>
<artifactId>native-image-maven-plugin</artifactId>
<version>0.10.4</version>
<configuration>
<imageName>myapp</imageName>
<mainClass>com.example.Main</mainClass>
<buildArgs>
<buildArg>--no-fallback</buildArg>
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
</buildArgs>
</configuration>
<executions>
<execution>
<id>build-native</id>
<phase>package</phase>
<goals>
<goal>native-image</goal>
</goals>
</execution>
</executions>
</plugin>
Terminal window
# Build with Maven
mvn -Pnative package
# Using the Gradle plugin
# build.gradle configuration:
plugins {
id 'org.graalvm.buildtools.native' version '0.10.4'
}
graalvmNative {
binaries {
main {
imageName = 'myapp'
mainClass = 'com.example.Main'
buildArgs.add('--no-fallback')
}
}
}
Terminal window
# Build with Gradle
gradle nativeCompile

Configuration: Reflection, Resources, Proxies, JNI

Section titled “Configuration: Reflection, Resources, Proxies, JNI”

The closed-world analysis is conservative. It cannot see reflective access, dynamic resource Loading, or runtime proxy generation. You must provide explicit configuration for these.

Reflection configuration (reflection-config.json):

[
{
"name": "com.example.MyDto",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredFields": true
}
]

Resource configuration (resource-config.json):

{
"resources": {
"includes": [{ "pattern": ".*\\.properties$" }, { "pattern": ".*\\.xml$" }]
}
}

Proxy configuration (proxy-config.json):

[["java.sql.Connection", "java.lang.AutoCloseable"]]

JNI configuration (jni-config.json):

[
{
"name": "com.example.NativeLib",
"methods": [{ "name": "nativeMethod", "parameterTypes": ["java.lang.String"] }]
}
]

The easiest way to generate these configuration files is using the native-image agent:

Terminal window
java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image \
-jar myapp.jar

Run the application through all code paths (especially reflection-heavy paths like serialization, Dependency injection, and ORM initialization). The agent writes the configuration files Automatically.

Class Initialization: Build Time vs Runtime

Section titled “Class Initialization: Build Time vs Runtime”

Native image classifies every class into one of two initialization times:

  • Build-time initialization: The class’s static initializer (&lt;clinit&gt;) runs during image generation. The resulting static field values are baked into the image heap.
  • Runtime initialization: The class’s static initializer runs at application startup, before main().

By default, most JDK classes are initialized at build time. Application classes are initialized at Runtime unless they are reachable from build-time-initialized classes.

### Conditional Feature Analysis

GraalVM native image supports conditional features through feature classes that hook into the build Process:

import org.graalvm.nativeimage.hosted.Feature;
public class MyFeature implements Feature {
@Override
public void beforeAnalysis(BeforeAnalysisAccess access) {
// Register classes, resources, proxies at build time
access.registerSubtype(access.findClassByName("com.example.MyInterface"));
}
@Override
public void duringAnalysis(DuringAnalysisAccess access) {
// Query reachability and register additional types
if (access.reachesType(access.findClassByName("com.example.OptionalFeature"))) {
access.registerSubtype(access.findClassByName("com.example.OptionalImpl"));
}
}
}

Register the feature in META-INF/native-image/native-image.properties:

Args = --features=org.graalvm.nativeimage.impl.InternalFeature \
--features=com.example.MyFeature

Native image eliminates JIT warmup entirely. All code is compiled ahead of time, and the image heap Contains pre-initialized objects. The result is startup times measured in single-digit milliseconds.

MetricHotSpot (JVM)Native ImageImprovement
Startup (cold)1-3 seconds5-20 ms100-500x
Time to first request2-5 seconds15-50 ms100-300x
RSS (idle)100-300 MB20-60 MB3-10x
RSS (peak)500 MB - 2 GB80-200 MB3-10x

Native image has no JIT compiler, no garbage collector metadata for JIT-compiled code, and a compact Image heap. The total memory footprint is 30-50 MB for a typical Spring Boot application, Compared to 200-500 MB on HotSpot.

The image heap is stored in the executable’s data segment and mapped into memory at process start. Objects in the image heap are not subject to garbage collection (they are effectively immortal), Which reduces GC pressure at runtime.

Native image does not match HotSpot’s peak throughput for long-running, CPU-intensive workloads. The Reasons are:

  1. No profile-guided JIT optimization: HotSpot collects profiling data during execution and recompiles hot methods with increasingly aggressive optimizations. Native image makes all optimization decisions at build time based on the agent’s profile, which may not cover all runtime scenarios.
  2. No speculative optimizations: HotSpot can speculate based on runtime profiles and deoptimize when speculation fails. Native image must produce code that works for all possible inputs.
  3. Fixed code layout: HotSpot can reoptimize code layout based on actual execution frequency. Native image’s code layout is fixed at build time.
WorkloadHotSpot ThroughputNative Image Throughput
I/O-bound REST APIBaseline90-110% of HotSpot
JSON parsingBaseline80-100% of HotSpot
Numeric computationBaseline60-85% of HotSpot
Crypto workloadsBaseline70-90% of HotSpot
Short-lived CLI toolN/A (warmup kills)10-100x faster overall
## Foreign Function and Memory API (Project Panama)

The Foreign Function and Memory API (JEP 424, standardized in Java 22) replaces JNI with a pure Java API for calling native functions and accessing native memory. It is safer, more ergonomic, and Supports variadic functions, structs by value, and callbacks without writing C glue code.

Definition. The FFM API consists of two main abstractions: Linker for creating downcall method Handles to native functions, and MemorySegment for modeling contiguous regions of native memory With deterministic deallocation.

TypePurpose
LinkerCreates native method handles (downcall/upcall)
SymbolLookupLooks up symbols in native libraries
MemorySegmentModels a contiguous region of native memory
ArenaControls the lifetime of memory segments
ValueLayoutDescribes the layout of values in memory
FunctionDescriptorDescribes the signature of a native function
MethodHandleInvokes a native function (downcall)
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.VarHandle;
public class CStringOperations {
public static void main(String[] args) throws Throwable {
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = SymbolLookup.libraryLookup("libc.so.6", Arena.ofAuto());
SymbolLookup lookup = name -> stdlib.lookup(name).or(Linker.nativeLinker().defaultLookup().lookup(name));
// strlen(const char *s) -> size_t
MethodHandle strlen = linker.downcallHandle(
lookup.lookup("strlen").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);
// Allocate a C string in native memory
try (Arena arena = Arena.ofConfined()) {
MemorySegment cString = arena.allocateFrom("Hello, native world!");
long length = (long) strlen.invoke(cString);
System.out.println("Length: " + length); // 20
}
}
}

C structs are modeled using GroupLayout:

import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.VarHandle;
public class StructExample {
// Equivalent to: struct Point { double x; double y; };
static final GroupLayout POINT_LAYOUT = MemoryLayout.structLayout(
ValueLayout.JAVA_DOUBLE.withName("x"),
ValueLayout.JAVA_DOUBLE.withName("y")
);
public static void main(String[] args) throws Throwable {
Linker linker = Linker.nativeLinker();
// Allocate a Point struct on the native heap
try (Arena arena = Arena.ofConfined()) {
MemorySegment point = arena.allocate(POINT_LAYOUT);
VarHandle xHandle = POINT_LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("x"));
VarHandle yHandle = POINT_LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("y"));
xHandle.set(point, 3.0);
yHandle.set(point, 4.0);
System.out.println("x=" + xHandle.get(point) + ", y=" + yHandle.get(point));
}
}
}

To pass a Java method as a C function pointer, create an upcall method handle:

import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
public class CallbackExample {
public static void main(String[] args) throws Throwable {
Linker linker = Linker.nativeLinker();
SymbolLookup lookup = name -> SymbolLookup.libraryLookup("libc.so.6", Arena.ofAuto())
.lookup(name)
.or(Linker.nativeLinker().defaultLookup().lookup(name));
// int compare(const void *a, const void *b, void *arg)
MethodHandle qsort = linker.downcallHandle(
lookup.lookup("qsort").orElseThrow(),
FunctionDescriptor.ofVoid(
ValueLayout.ADDRESS, // base
ValueLayout.JAVA_LONG, // nmemb
ValueLayout.JAVA_LONG, // size
ValueLayout.ADDRESS // compar
)
);
// Callback: int (*compar)(const void *, const void *)
MethodHandle comparHandle = MethodHandles.lookup().findStatic(
CallbackExample.class,
"compareInts",
MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class)
);
MemorySegment comparFunc = linker.upcallStub(
comparHandle,
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS),
Arena.ofAuto()
);
try (Arena arena = Arena.ofConfined()) {
MemorySegment array = arena.allocate(ValueLayout.JAVA_INT, 5);
VarHandle intHandle = ValueLayout.JAVA_INT.varHandle();
intHandle.set(array, 0L, 5);
intHandle.set(array, 1L, 2);
intHandle.set(array, 2L, 8);
intHandle.set(array, 3L, 1);
intHandle.set(array, 4L, 9);
qsort.invoke(array, 5L, 4L, comparFunc);
System.out.print("Sorted: ");
for (long i = 0; i &lt; 5; i++) {
System.out.print(intHandle.get(array, i) + " ");
}
// Output: Sorted: 1 2 5 8 9
}
}
static int compareInts(MemorySegment a, MemorySegment b) {
int va = ValueLayout.JAVA_INT.get(a, 0);
int vb = ValueLayout.JAVA_INT.get(b, 0);
return Integer.compare(va, vb);
}
}

Downcall and Upcall: Complete Example (libcurl)

Section titled “Downcall and Upcall: Complete Example (libcurl)”
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.invoke.VarHandle;
public class LibCurlExample {
static final Linker LINKER = Linker.nativeLinker();
static final SymbolLookup CURL = SymbolLookup.libraryLookup(
"libcurl.so.4", Arena.ofAuto()
);
static final MemorySession.SessionImpl CURL_GLOBAL_ALL =
MemorySession.sessionCreator().create();
public static void main(String[] args) throws Throwable {
// curl_easy_init() -> CURL*
MethodHandle curlEasyInit = LINKER.downcallHandle(
CURL.lookup("curl_easy_init").orElseThrow(),
FunctionDescriptor.of(ValueLayout.ADDRESS)
);
// curl_easy_setopt(CURL*, CURLoption, ...) -> CURLcode
MethodHandle curlEasySetopt = LINKER.downcallHandle(
CURL.lookup("curl_easy_setopt").orElseThrow(),
FunctionDescriptor.of(
ValueLayout.JAVA_INT,
ValueLayout.ADDRESS,
ValueLayout.JAVA_INT,
ValueLayout.ADDRESS
)
);
// curl_easy_perform(CURL*) -> CURLcode
MethodHandle curlEasyPerform = LINKER.downcallHandle(
CURL.lookup("curl_easy_perform").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS)
);
// curl_easy_cleanup(CURL*) -> void
MethodHandle curlEasyCleanup = LINKER.downcallHandle(
CURL.lookup("curl_easy_cleanup").orElseThrow(),
FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)
);
try (Arena arena = Arena.ofConfined()) {
MemorySegment curl = (MemorySegment) curlEasyInit.invoke();
// CURLOPT_URL = 10002
MemorySegment url = arena.allocateFrom("https://example.com");
curlEasySetopt.invoke(curl, 10002, url);
int result = (int) curlEasyPerform.invoke(curl);
curlEasyCleanup.invoke(curl);
System.out.println("curl result: " + result);
}
}
}
FeatureJNIFFM API
BoilerplateC header generation + C codePure Java
Type safetyWeak (jobject, jfieldID)Strong (MemorySegment, ValueLayout)
Memory managementManual (DeleteLocalRef, etc.)Arena-based, deterministic deallocation
Variadic functionsNot supportedSupported via FunctionDescriptor
Structs by valueNot supportedSupported via GroupLayout
CallbacksManual JNI env + C trampolineUpcall handles
PerformanceGood (direct call after linking)Comparable (downcall stubs are fast)
SafetyUndefined behavior on misuseBounds-checked, null-checked
## Foreign Memory Access

The Arena class controls the lifetime of memory segments. When an arena is closed, all segments Allocated from it are freed. This eliminates the class of bugs where native memory is leaked because The Java code forgot to call free().

import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
public class ArenaExample {
public static void main(String[] args) {
// Confined arena: only one thread can access its segments
try (Arena arena = Arena.ofConfined()) {
MemorySegment buffer = arena.allocate(1024);
ValueLayout.JAVA_INT.set(buffer, 0L, 42);
int value = ValueLayout.JAVA_INT.get(buffer, 0L);
System.out.println("Value: " + value); // 42
}
// buffer is freed here
// Shared arena: multiple threads can access segments
try (Arena arena = Arena.ofShared()) {
MemorySegment shared = arena.allocate(256);
// safe to pass 'shared' to another thread
}
// Auto arena: freed by the GC (for interop with existing code)
Arena autoArena = Arena.ofAuto();
MemorySegment ephemeral = autoArena.allocate(128);
// freed when ephemeral becomes unreachable
}
}
import java.lang.foreign.*;
import java.lang.invoke.VarHandle;
import java.nio.ByteOrder;
public class MemorySegmentDemo {
public static void main(String[] args) {
try (Arena arena = Arena.ofConfined()) {
// Allocate from a C string literal
MemorySegment hello = arena.allocateFrom("Hello, World!");
long byteSize = hello.byteSize(); // 14 (13 chars + null terminator)
// Allocate a typed array in native memory
MemorySegment doubles = arena.allocate(ValueLayout.JAVA_DOUBLE, 10);
for (long i = 0; i &lt; 10; i++) {
ValueLayout.JAVA_DOUBLE.set(doubles, i, i * 1.5);
}
// Bulk copy between segments
MemorySegment dest = arena.allocate(80);
dest.copyFrom(doubles);
// Slice a segment
MemorySegment firstFive = doubles.asSlice(0, 5 * ValueLayout.JAVA_DOUBLE.byteSize());
// Fill with a value
MemorySegment zeros = arena.allocate(256);
zeros.fill((byte) 0);
// Create a mapped segment from a file
MemorySegment fileData = MemorySegment.mapFile(
Path.of("data.bin"),
0L,
1024,
FileChannel.MapMode.READ_ONLY,
Arena.ofAuto()
);
} catch (Exception e) {
e.printStackTrace();
}
}
}

ValueLayout describes how values are laid out in memory. Each primitive type has a corresponding Layout with a specific byte order and alignment:

import java.lang.foreign.ValueLayout;
import java.nio.ByteOrder;
// Standard layouts (platform byte order)
ValueLayout.JAVA_BYTE // 1 byte
ValueLayout.JAVA_SHORT // 2 bytes
ValueLayout.JAVA_CHAR // 2 bytes (unsigned)
ValueLayout.JAVA_INT // 4 bytes
ValueLayout.JAVA_LONG // 8 bytes
ValueLayout.JAVA_FLOAT // 4 bytes
ValueLayout.JAVA_DOUBLE // 8 bytes
ValueLayout.ADDRESS // Platform-dependent pointer size (4 or 8 bytes)
// Custom layouts with explicit byte order
ValueLayout.JAVA_INT.withOrder(ByteOrder.LITTLE_ENDIAN)
// C-compatible layouts
ValueLayout.C_BOOL // _Bool (1 byte)
ValueLayout.C_SHORT // short (2 bytes)
ValueLayout.C_INT // int (4 bytes)
ValueLayout.C_LONG // long (4 or 8 bytes, platform-dependent)
ValueLayout.C_LONG_LONG // long long (8 bytes)
ValueLayout.C_FLOAT // float (4 bytes)
ValueLayout.C_DOUBLE // double (8 bytes)
ValueLayout.C_POINTER // pointer (4 or 8 bytes)

VarHandle provides atomic and volatile access to memory segments, integrating with the Java memory Model:

import java.lang.foreign.*;
import java.lang.invoke.VarHandle;
import java.util.concurrent.atomic.AtomicInteger;
public class VarHandleMemoryDemo {
public static void main(String[] args) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment counter = arena.allocate(ValueLayout.JAVA_INT);
VarHandle handle = ValueLayout.JAVA_INT.varHandle();
// Plain access
handle.set(counter, 0L, 0);
// Volatile write and read
handle.setVolatile(counter, 0L, 42);
int val = (int) handle.getVolatile(counter, 0L);
// Compare-and-swap
boolean success = handle.compareAndSet(counter, 0L, 42, 43);
// Atomic add
int oldVal = (int) handle.getAndAdd(counter, 0L, 10);
}
}
}

Memory segments enable building high-performance data structures outside the Java heap, avoiding GC Pressure:

import java.lang.foreign.*;
public class OffHeapRingBuffer {
private final MemorySegment buffer;
private final long capacity;
private final VarHandle intHandle;
private long head = 0;
private long tail = 0;
public OffHeapRingBuffer(int capacity) {
this.capacity = capacity;
try (Arena arena = Arena.ofConfined()) {
this.buffer = arena.allocate(ValueLayout.JAVA_LONG, capacity);
}
this.intHandle = ValueLayout.JAVA_LONG.varHandle();
}
public void offer(long value) {
intHandle.set(buffer, tail * ValueLayout.JAVA_LONG.byteSize(), value);
tail = (tail + 1) % capacity;
}
public long poll() {
long value = (long) intHandle.get(buffer, head * ValueLayout.JAVA_LONG.byteSize());
head = (head + 1) % capacity;
return value;
}
public boolean isEmpty() {
return head == tail;
}
}
## Vector API (Incubator)

The Vector API (JEP 448, incubating) provides SIMD (Single Instruction, Multiple Data) operations in Java. It allows developers to express data-parallel computations that the JVM maps to hardware SIMD Instructions (AVX2, AVX-512, NEON, SVE) without writing intrinsics or architecture-specific code.

Definition. A Vector&lt;E&gt; represents a fixed number of values of a primitive type E (byte, short, int, long, float, double) packed into a single hardware register. A VectorSpecies&lt;E&gt; defines the shape (lane count and bit width) of the vector.

import jdk.incubator.vector.*;
public class VectorDemo {
public static void floatVectorAdd(float[] a, float[] b, float[] result) {
// Select the preferred vector species for float (256-bit on most x86 machines)
VectorSpecies&lt;Float&gt; species = FloatVector.SPECIES_PREFERRED;
int i = 0;
// Process 8 floats at a time (256 bits / 32 bits per float = 8 lanes)
int upperBound = species.loopBound(a.length);
for (; i &lt; upperBound; i += species.length()) {
FloatVector va = FloatVector.fromArray(species, a, i);
FloatVector vb = FloatVector.fromArray(species, b, i);
va.add(vb).intoArray(result, i);
}
// Handle remainder
for (; i &lt; a.length; i++) {
result[i] = a[i] + b[i];
}
}
public static void main(String[] args) {
float[] a = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f};
float[] b = {8.0f, 7.0f, 6.0f, 5.0f, 4.0f, 3.0f, 2.0f, 1.0f};
float[] result = new float[8];
floatVectorAdd(a, b, result);
// result: [9.0, 9.0, 9.0, 9.0, 9.0, 9.0, 9.0, 9.0]
}
}
import jdk.incubator.vector.*;
public class LaneWiseOperations {
public static void vectorOperations(float[] a, float[] b, float[] result) {
VectorSpecies&lt;Float&gt; species = FloatVector.SPECIES_PREFERRED;
int i = 0;
int upperBound = species.loopBound(a.length);
for (; i &lt; upperBound; i += species.length()) {
FloatVector va = FloatVector.fromArray(species, a, i);
FloatVector vb = FloatVector.fromArray(species, b, i);
// Lane-wise multiply, then add a scalar
FloatVector mul = va.mul(vb);
FloatVector addScalar = mul.add(1.0f);
// Lane-wise comparison, producing a mask
VectorMask&lt;Float&gt; mask = va.gt(vb);
// Blend: select from va where mask is true, from vb where false
FloatVector blended = va.blend(vb, mask);
// Reduce: sum all lanes to a single float
float sum = va.reduceLanes(VectorOperators.ADD);
blended.intoArray(result, i);
}
for (; i &lt; a.length; i++) {
result[i] = a[i];
}
}
}

For compute-intensive loops operating on arrays of primitives, the Vector API can provide 4-8x Speedup over scalar code on hardware with AVX2/AVX-512 support. The key advantage over manual Intrinsics is portability: the same Java code runs on x86 (AVX2/AVX-512), ARM (NEON/SVE), and other Architectures, with the JIT compiler selecting the appropriate instructions at runtime.

## Virtual Threads (Project Loom)

Virtual threads are covered in depth in Virtual Threads and Structured Concurrency. This section provides a condensed reference.

  • JEP 444, Java 21: Virtual threads are a standard feature.
  • M:N scheduling: Many virtual threads multiplex onto a small pool of carrier (OS) threads.
  • Heap-allocated stacks: Virtual thread stacks are continuations on the heap, not 1 MB stack segments. A blocked virtual thread costs a few hundred bytes.
  • Transparent blocking: Thread.sleepSocket I/O, file I/O, and Future.get automatically unmount the virtual thread from its carrier.
// Create a single virtual thread
Thread vt = Thread.ofVirtual().name("worker").start(() -> doWork());
// Executor that creates a new virtual thread per task
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> blockingIoCall());
}
// Avoid synchronized blocks for blocking operations (pinning)
// Bad: pins the carrier thread
synchronized (lock) { socket.read(buffer); }
// Good: ReentrantLock allows unmounting
lock.lock();
try { socket.read(buffer); } finally { lock.unlock(); }
ScenarioUse Virtual Threads?
HTTP server (I/O-bound)Yes
Database query orchestrationYes
File processing pipelineYes
CPU-bound computationNo (use parallel streams or ForkJoinPool)
Latency-critical pathNo (scheduling adds overhead)

Structured concurrency is covered in depth in Virtual Threads and Structured Concurrency. This section provides a condensed reference.

  • JEP 453, Java 21: Structured concurrency is a preview API.
  • StructuredTaskScope: Enforces parent-child task relationships. All children must complete before the parent proceeds.
  • Shutdown policies: ShutdownOnFailure (cancel all on first error) and ShutdownOnSuccess (cancel all on first success).
  • Thread dumps: Structured concurrency produces thread dumps that show the parent-child relationship, making it easy to understand what a thread is waiting for.
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var userTask = scope.fork(() -> fetchUser(id));
var ordersTask = scope.fork(() -> fetchOrders(id));
scope.join();
scope.throwIfFailed();
return new UserData(userTask.get(), ordersTask.get());
}

Record patterns and pattern matching for switch are covered in depth in Records, Sealed Classes, and Pattern Matching. This section provides a condensed reference.

  • JEP 440, Java 21: Record patterns are a standard feature.
  • JEP 441, Java 21: Pattern matching for switch is a standard feature.
  • Record deconstruction: Destructure records directly in instanceof and switch patterns.
  • Exhaustiveness: The compiler verifies that all cases of a sealed hierarchy are covered.
// Record pattern in instanceof
if (obj instanceof Point(var x, var y)) {
System.out.println("Point at (" + x + ", " + y + ")");
}
// Record pattern in switch with sealed hierarchy
return switch (shape) {
case Circle(var r) when r == 0 -> 0.0;
case Circle(var r) -> Math.PI * r * r;
case Rectangle(var w, var h) -> w * h;
case Triangle t -> heronsFormula(t);
};

Project Valhalla introduces value types to Java — classes that have identity-free, flattened Instances. A value type’s instances are compared by their contents, not by reference identity. They Can be stored inline in fields and array elements, eliminating the memory indirection and cache Misses of boxed types.

Definition. A value class is a class declared with the value keyword (or inline in earlier Prototypes). Its instances have no identity — there is no concept of reference equality, no Synchronization, and no null instances. Two value instances with the same field values are Considered equal.

### Identity Classes vs Value Classes
PropertyIdentity Class (today)Value Class (Valhalla)
IdentityHas identity (==)No identity
NullabilityCan be nullCannot be null
Default valuenullAll-zero bits (like int 0)
Memory layoutReference (pointer)Flattened inline
== operatorReference equalityStructural equality
SynchronizationCan use synchronizedNot allowed
SupertypeObjectImplicit abstract superclass
Generic type paramsReference types onlyCan be used as type argument
// Value class (Valhalla preview syntax)
public value class Point {
final double x;
final double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
}
// Usage: Point is stored inline, no heap allocation
public value class Line {
final Point start;
final Point end;
public Line(Point start, Point end) {
this.start = start;
this.end = end;
}
}

A Line contains two Point instances inline — there are no pointers. The entire Line is 32 Bytes (four doubles), stored contiguously in memory. Compare this to the identity class equivalent, Which would be 16 bytes of references plus 32 bytes of heap-allocated Point objects.

// Identity class: array of references (each element is a pointer)
Point[] identityPoints = new Point[1000];
// Memory: 8000 bytes for references + 16000 bytes for Point objects on heap
// Value class: array of flattened values (no indirection)
value Point[] valuePoints = new value Point[1000];
// Memory: 16000 bytes total (8 bytes per element, inline in the array)

The flattened layout eliminates one level of indirection, which improves cache utilization for Data-intensive workloads. For double[] arrays, the difference is negligible (doubles are already Primitive), but for arrays of small compound types (points, complex numbers, 2D/3D vectors), the Improvement is significant.

Value types solve a long-standing limitation of Java generics: primitive types cannot be used as Type arguments. With value types, List&lt;Point&gt; can store Point instances inline (or at Least more compactly) without boxing.

// Today: List<int> does not exist, must use List<Integer> (boxed)
List&lt;Integer&gt; boxed = new ArrayList&lt;&gt;();
boxed.add(42); // allocates an Integer object
// Valhalla: List<int> stores int values directly
List&lt;int&gt; unboxed = new ArrayList&lt;&gt;(); // hypothetical syntax
unboxed.add(42); // no allocation

The exact generic specialization strategy is still under discussion. The current Valhalla prototype Uses a “value generic” approach where type arguments can be value types, and the JVM generates Specialized code paths for value-carrying generic classes.

Valhalla has been in development for over a decade. As of JDK 23:

  • Value classes: Preview in recent builds. Syntax may change.
  • Generic specialization: Not yet implemented. This is the hardest part of Valhalla.
  • Migration: Existing wrapper types (Integer``LongEtc.) will eventually be migrated to value classes, but this is a long-term goal that requires full generic specialization.
Terminal window
# Using SDKMAN (recommended)
sdk install java 22-graal
sdk use java 22-graal
# Or download directly from https://github.com/graalvm/graalvm-ce/releases
# Verify installation
java -version
# openjdk version "22.0.2" 2024-...
# OpenJDK Runtime Environment GraalVM CE 22.0.2
native-image --version
# native-image 22.0.2
### Building a Native Image: Step by Step

Step 1: Create a simple Java application:

src/main/java/com/example/App.java
package com.example;
public class App {
public static void main(String[] args) {
System.out.println("Hello from native image!");
System.out.println("OS: " + System.getProperty("os.name"));
System.out.println("Arch: " + System.getProperty("os.arch"));
}
}

Step 2: Compile:

Terminal window
javac -d target/classes src/main/java/com/example/App.java

Step 3: Build native image:

Terminal window
native-image -H:Name=app -cp target/classes com.example.App

Step 4: Run:

Terminal window
./app
# Hello from native image!
# OS: Linux
# Arch: amd64

For applications that use reflection (Spring, Jackson, Hibernate, etc.), use the agent to collect Metadata:

Terminal window
# Phase 1: Run with agent to collect configuration
java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image \
-jar myapp.jar
# Phase 2: Build native image with collected configuration
native-image -H:ConfigurationFileDirectories=src/main/resources/META-INF/native-image \
-H:Name=myapp \
-jar myapp.jar

For Spring Boot applications, the Spring AOT plugin handles most of this automatically:

<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>paketobuildpacks/builder-jammy-base:latest</builder>
<env>
<BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
</env>
</image>
</configuration>
</plugin>
Terminal window
mvn spring-boot:build-image -Pnative
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class NativeImageTest {
@Test
void testMainOutput() {
// Test the main class logic, not the native image itself
App app = new App();
String result = app.greet("World");
assertEquals("Hello, World!", result);
}
}
Terminal window
# Run tests in JVM mode (fast iteration)
mvn test
# Build native image and run integration tests against it
mvn -Pnative native:compile
./target/myapp --test