2026/04/27

FFMForeign Function and Memory (FFM) API

為了使用外部函式庫,java 需要跟原生的 native code 進行互動,傳統是使用 Java Native Interface JNI 進行開發,但 JNI 的開發過程繁複,需要定義 native method,然後從 Java Source Code 生成 C 的 header file,再用C 語言實作,然後才能編譯並連結原生開發的函式庫。

Foreign Function & Memory API(FFM API)參考了其他語言的方法,實作外部函式庫的 Interface,提供更安全有效率的方法,存取本地端的記憶體及函式,取代了 JNI 的功能。這是自 JDK 22 開始的一組新的 API,用來取代 JNI。能夠呼叫 C/C++/Rust 的非 Java 原生函式庫,可直接操作記憶體。

JNI sample

JNIDemo.java

public class JNIDemo {
    static {
        System.loadLibrary("JNIDemo");
    }

    public static void main(String[] args) {
        new JNIDemo().printHelloWorld();
    }

    private native void printHelloWorld();
}

這裡面,有一個 private native void printHelloWorld(),這就是還沒有實作的原生函式。

用以下指令,編譯 class 並產生 header file

javac -h . JNIDemo.java

JNIDemo.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JNIDemo */

#ifndef _Included_JNIDemo
#define _Included_JNIDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     JNIDemo
 * Method:    printHelloWorld
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_JNIDemo_printHelloWorld
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

編譯 C library

因為在 macos 先查詢 JAVA_HOME 目錄

$ /usr/libexec/java_home
/opt/local/Library/Java/JavaVirtualMachines/jdk-25-azul-zulu.jdk/Contents/Home

編譯

JAVA_HOME=/opt/local/Library/Java/JavaVirtualMachines/jdk-25-azul-zulu.jdk/Contents/Home
gcc -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin -dynamiclib JNIDemo.c -o libJNIDemo.dylib

會產生 libJNIDemo.dylib

執行

$ java -cp . -Djava.library.path=. JNIDemo
WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::loadLibrary has been called by JNIDemo in an unnamed module (file:/Users/charley/Downloads/)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

Hello World!

FFM

FFM API 能夠讓 Java 程式在不使用 JNI 的情況下

  • 更安全地存取記憶體

  • 直接呼叫 C function

  • 呼叫 C/系統的 library

FFM 核心元件:

  • Linker:對應 C 語言呼叫約定的連結器,用來呼叫 native 函式

  • SymbolLookup: 在 native library 或 process 中查找符號(例如 printf

  • MemorySegment: 表示一塊連續的記憶體(on-heap 或 off-heap)

  • Arena:管理 MemorySegment 的生命週期(自動釋放資源)

  • ValueLayout:描述原生型別的記憶體布局,例如 JAVA_INT, C_DOUBLE

  • FunctionDescriptor:描述 native 函式的簽章(參數與回傳型別)

測試1

測試呼叫 strlen

import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.nio.charset.StandardCharsets;

public class FFMStrlenDemo {
    public static void main(String[] args) throws Throwable {
        // 1. 取得系統的 Linker(如 SystemV ABI 或 Windows ABI)
        Linker linker = Linker.nativeLinker();

        // 2. 找出 symbol (在 libc 裡)
        SymbolLookup stdlib = Linker.nativeLinker().defaultLookup();
        MemorySegment strlenAddr = stdlib.find("strlen").orElseThrow();

        // 3. 建立 Java 對應的 MethodHandle
        MethodHandle strlen = linker.downcallHandle(
            strlenAddr,
            FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
        );

        // 4. 建立要傳入的字串記憶體
        // try (Arena arena = Arena.ofConfined()) {
        //     MemorySegment str = arena.allocateUtf8String("Hello FFM API!");
        //     long len = (long) strlen.invoke(str);
        //     System.out.println("Length = " + len);
        // }
        try (Arena arena = Arena.ofConfined()) {
            // 手動建立 UTF-8 null-terminated string
            byte[] bytes = "Hello FFM API!".getBytes(StandardCharsets.UTF_8);
            MemorySegment str = arena.allocate(bytes.length + 1, 1);
            str.asSlice(0, bytes.length).copyFrom(MemorySegment.ofArray(bytes));
            str.set(ValueLayout.JAVA_BYTE, bytes.length, (byte) 0); // '\0'

            long len = (long) strlen.invoke(str);
            System.out.println("Length = " + len);
        }

        // allocate set/get 測試
        try (Arena arena = Arena.ofConfined()) {
            MemorySegment seg = arena.allocate(ValueLayout.JAVA_INT);
            seg.set(ValueLayout.JAVA_INT, 0, 42); // 寫入 42
            int value = seg.get(ValueLayout.JAVA_INT, 0);
            System.out.println("Read value: " + value);
        } // arena 自動釋放記憶體
    }
}

檢查 allocate API

標準 API 是 java.lang.foreign.Arena.allocateUtf8String(String)

但測試,結果是 allocate

# javap -classpath $JAVA_HOME/lib/modules java.lang.foreign.Arena | grep allocate

public abstract java.lang.foreign.MemorySegment allocate(long, long);

這表示目前這個 openjdk 還是使用舊版的 allocate,缺少 allocateUtf8String

編譯,執行

javac FFMStrlenDemo.java
java --enable-native-access=ALL-UNNAMED FFMStrlenDemo

結果

Length = 14
Read value: 42

測試2

sum.c

// sum.c
#include <stdio.h>

int sum(int a, int b) {
    return a + b;
}

編譯為 library

## linux
# gcc -shared -fPIC -o libsum.so sum.c

## macos
gcc -shared -fPIC -o libsum.dylib sum.c

SumFFMDemo.java 放在同一個目錄

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

public class SumFFMDemo {
    public static void main(String[] args) throws Throwable {
        // 載入我們的動態函式庫
        System.loadLibrary("sum"); // 對應 libsum.so / sum.dll

        // 建立 linker
        Linker linker = Linker.nativeLinker();

        // 查找 symbol
        SymbolLookup lookup = SymbolLookup.libraryLookup("libsum.dylib", Arena.global());
        MemorySegment funcAddr = lookup.find("sum").orElseThrow();

        // 建立函式描述: int (int, int)
        FunctionDescriptor fd = FunctionDescriptor.of(
                ValueLayout.JAVA_INT, // return type
                ValueLayout.JAVA_INT, // param a
                ValueLayout.JAVA_INT  // param b
        );

        // 建立 method handle
        MethodHandle sum = linker.downcallHandle(funcAddr, fd);

        // 呼叫 native 函式
        int result = (int) sum.invoke(12, 30);
        System.out.println("sum(12, 30) = " + result);
    }
}

編譯

javac SumFFMDemo.java
java --enable-native-access=ALL-UNNAMED SumFFMDemo
# java -Djava.library.path=. --enable-native-access=ALL-UNNAMED SumFFMDemo

結果

sum(12, 30) = 42

2026/04/20

Compact Object Header

在 JDK 25 中,Compact Object Headers(緊湊型物件標頭)已經正式成為 HotSpot JVM 的預設功能,並且不再需要使用 -XX:+UseCompactObjectHeaders 啟用這個功能。這是 JEP 519 的一部分,用途是進一步優化 Java 物件的記憶體佈局。在 JDK 24 中,這個功能曾作為實驗性功能引入,在 JDK 25 中轉為正式功能。

在 64 位架構的 HotSpot JVM 中,物件標頭的大小從原本的 12 至 16 字節(取決於 JVM 配置)縮減至 8 字節(64 位)。

  • 減少記憶體佔用:物件標頭變小,整體記憶體佔用降低。

  • 提高快取效率:更緊湊的記憶體佈局有助於提升 CPU 快取的命中率。

  • 降低垃圾回收壓力:減少記憶體佔用量,有助於減少垃圾回收的次數。

  • 提升部署密度:在容器化環境中,減少記憶體佔用量有助於提高部署密度。

Project Lilliput 的目標是將物件標頭的大小進一步縮小至 4 字節。然而,這樣的改變需要更深入的研究和測試,以確保不會影響 JVM 的穩定性和性能。

測試

import java.lang.management.ManagementFactory;
import java.lang.management.MemoryPoolMXBean;
import java.lang.management.MemoryUsage;
import java.util.ArrayList;
import java.util.List;

/**
 * JDK 25 Compact Object Header 功能測試
 * 
 * 功能說明:
 * Compact Object Header (JEP 450) 是 JDK 25 引入的實驗性特性
 * 目的是減少物件頭的記憶體開銷,從傳統的 12-16 bytes 減少到 8 bytes
 * 
 * 啟用方式:
 * java -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders YourClass
 * 
 * 主要優勢:
 * 1. 減少記憶體佔用(每個物件節省 4-8 bytes)
 * 2. 提升快取效率(更好的資料局部性)
 * 3. 適合大量小物件的應用場景
 */
public class CompactObjectHeaderTest {

    private static final int OBJECT_COUNT = 1_000_000;

    static class SmallObject {
        private int id;
        private String name;

        public SmallObject(int id, String name) {
            this.id = id;
            this.name = name;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== JDK 25 Compact Object Header 測試 ===\n");

        // 檢查 JVM 參數
        checkJVMFlags();

        // 顯示初始記憶體狀態
        System.out.println("\n--- 初始記憶體狀態 ---");
        printMemoryUsage();

        // 建立大量物件並測試
        System.out.println("\n--- 建立 " + OBJECT_COUNT + " 個物件 ---");
        List<SmallObject> objects = new ArrayList<>(OBJECT_COUNT);

        long startTime = System.currentTimeMillis();
        long startMemory = getUsedMemory();

        for (int i = 0; i < OBJECT_COUNT; i++) {
            objects.add(new SmallObject(i, "Object_" + i));
        }

        long endTime = System.currentTimeMillis();
        long endMemory = getUsedMemory();

        // 強制垃圾回收以獲得更準確的記憶體使用量
        System.gc();
        Thread.sleep(100);
        long afterGCMemory = getUsedMemory();

        // 顯示測試結果
        System.out.println("\n--- 測試結果 ---");
        System.out.println("建立時間: " + (endTime - startTime) + " ms");
        System.out.println("記憶體增量 (建立後): " + formatBytes(endMemory - startMemory));
        System.out.println("記憶體增量 (GC後): " + formatBytes(afterGCMemory - startMemory));
        System.out.println("平均每個物件: " + formatBytes((afterGCMemory - startMemory) / OBJECT_COUNT));

        // 顯示最終記憶體狀態
        System.out.println("\n--- 最終記憶體狀態 ---");
        printMemoryUsage();

        // 物件頭大小估算
        System.out.println("\n--- 物件頭分析 ---");
        analyzeObjectHeader(afterGCMemory - startMemory);

        // 保持物件存活
        System.out.println("\n物件數量: " + objects.size());
        System.out.println("\n提示: 使用 -XX:+UseCompactObjectHeaders 啟用壓縮物件頭");
        System.out.println("比較指令: java -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders CompactObjectHeaderTest");
    }

    private static void checkJVMFlags() {
        System.out.println("JVM 版本: " + System.getProperty("java.version"));
        System.out.println("JVM 供應商: " + System.getProperty("java.vendor"));

        List<String> inputArgs = ManagementFactory.getRuntimeMXBean().getInputArguments();
        System.out.println("\nJVM 參數:");
        for (String arg : inputArgs) {
            System.out.println("  " + arg);
        }

        boolean hasCompactHeadersFlag = inputArgs.stream()
            .anyMatch(arg -> arg.contains("UseCompactObjectHeaders"));
        boolean isEnabled = inputArgs.stream()
            .anyMatch(arg -> arg.contains("+UseCompactObjectHeaders"));
        boolean isDisabled = inputArgs.stream()
            .anyMatch(arg -> arg.contains("-UseCompactObjectHeaders"));

        if (isEnabled) {
            System.out.println("\n✓ Compact Object Headers 已明確啟用 (+UseCompactObjectHeaders)");
        } else if (isDisabled) {
            System.out.println("\n✗ Compact Object Headers 已明確禁用 (-UseCompactObjectHeaders)");
        } else if (hasCompactHeadersFlag) {
            System.out.println("\n? Compact Object Headers 參數已設定");
        } else {
            System.out.println("\n- Compact Object Headers 使用預設設定");
        }
    }

    private static void printMemoryUsage() {
        Runtime runtime = Runtime.getRuntime();
        long totalMemory = runtime.totalMemory();
        long freeMemory = runtime.freeMemory();
        long usedMemory = totalMemory - freeMemory;
        long maxMemory = runtime.maxMemory();

        System.out.println("已使用記憶體: " + formatBytes(usedMemory));
        System.out.println("總分配記憶體: " + formatBytes(totalMemory));
        System.out.println("最大可用記憶體: " + formatBytes(maxMemory));
        System.out.println("可用記憶體: " + formatBytes(freeMemory));
    }

    private static long getUsedMemory() {
        Runtime runtime = Runtime.getRuntime();
        return runtime.totalMemory() - runtime.freeMemory();
    }

    private static String formatBytes(long bytes) {
        if (bytes < 1024) return bytes + " B";
        if (bytes < 1024 * 1024) return String.format("%.2f KB", bytes / 1024.0);
        if (bytes < 1024 * 1024 * 1024) return String.format("%.2f MB", bytes / (1024.0 * 1024));
        return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024));
    }

    private static void analyzeObjectHeader(long totalMemory) {
        // 估算物件頭大小
        // 傳統物件頭: 12 bytes (32-bit) 或 16 bytes (64-bit 壓縮指標)
        // Compact 物件頭: 8 bytes

        long avgBytesPerObject = totalMemory / OBJECT_COUNT;

        System.out.println("平均每個物件記憶體: " + avgBytesPerObject + " bytes");
        System.out.println("\n理論估算:");
        System.out.println("  - 傳統物件頭: 16 bytes (mark word + klass pointer)");
        System.out.println("  - Compact 物件頭: 8 bytes");
        System.out.println("  - int 欄位: 4 bytes");
        System.out.println("  - String 參考: 4-8 bytes (壓縮指標)");
        System.out.println("  - 對齊填充: 可能需要額外空間");

        if (avgBytesPerObject <= 70) {
            System.out.println("\n✓ 可能正在使用 Compact Object Headers");
        } else {
            System.out.println("\n✗ 可能使用傳統物件頭");
        }
    }
}

編譯後,用這兩種方式執行

# 禁用 Compact Object Headers
java -XX:-UseCompactObjectHeaders -Xms512m -Xmx512m CompactObjectHeaderTest > test_without_compact.log 2>&1
# 啟用
java -XX:+UseCompactObjectHeaders -Xms512m -Xmx512m CompactObjectHeaderTest > test_with_compact.log 2>&1

執行結果比較

傳統模式 - 平均每個物件: 78
壓縮模式 - 平均每個物件: 69

分析

傳統模式 (78 bytes):
├─ 物件頭:16 bytes (mark word 8 + klass pointer 8)
├─ int id:4 bytes
├─ String 參考:8 bytes (未壓縮指標)
└─ 對齊填充:~50 bytes (String 物件本身的開銷)

壓縮模式 (69 bytes):
├─ 物件頭:8 bytes (壓縮後)
├─ int id:4 bytes
├─ String 參考:8 bytes
└─ 對齊填充:~49 bytes

header 部分減少了 8 bytes,實際上節省 9 bytes

如果 application 是大量的小物件的集合,例如 cache,就啟用-XX:+UseCompactObjectHeaders

2026/03/30

Scoped Values

Scoped Values 是一種在特定執行範圍內(scope)安全地共享不可變資料的方法。主要用來取代 ThreadLocal,適合用在 Virtual Thread。

ThreadLocal vs ScopedValue

ThreadLocal

  • 為每個 Thread 儲存獨立的可變狀態,每個 Thread 都有自己的ThreadLocalMap。

  • ThreadLocal 狀態同步代價高

  • 傳統、可變、不安全於虛擬執行緒

  • 需要手動清理 (remove())

  • 同一 Thread 被reuse(例如在 Executor 裡)會殘留前一次的資料。

  • 使用 Thread PoolVirtual Thread 時容易出錯

  • 每個 虛擬執行緒 都有自己的 ThreadLocal Map

ScopedValue

  • 在限定作用範圍中傳遞不可變資料

  • 不可變、安全、快速

  • 易於清理,離開 scope 即失效。結束作用域後自動清理

  • 支援虛擬執行緒、巢狀範圍與 Structured Concurrency

Demo

如果你希望把 ThreadLocal 值「傳遞」給虛擬執行緒,有兩種方法:

  1. 手動傳遞:在建立虛擬執行緒時,讀取父 ThreadLocal,然後在子執行緒裡設置。

  2. 使用 InheritableThreadLocal:Java 提供 InheritableThreadLocal,可以讓子執行緒自動繼承父執行緒的值。

    • 注意:對 虛擬執行緒,InheritableThreadLocal 仍然可用,但它只在 創建時 繼承,後續變更不會同步。
import java.util.concurrent.*;
import java.lang.ScopedValue;
import java.time.Duration;

public class ScopedValueVsThreadLocalDemo {

    private static final ThreadLocal<String> TL_USER = new ThreadLocal<>();
    private static final ScopedValue<String> SV_USER = ScopedValue.newInstance();

    private static final InheritableThreadLocal<String> ITL_USER = new InheritableThreadLocal<>();

    public static void main(String[] args) throws Exception {
        System.out.println("\n=== ThreadLocal 測試 ===");
        testThreadLocal();

        System.out.println("\n=== ThreadLocal 測試2 ===");
        testThreadLocal2();

        System.out.println("\n=== InheritableThreadLocal 測試3 ===");
        testThreadLocal3();

        System.out.println("\n=== ScopedValue 測試 ===");
        testScopedValue();
    }

    private static void testThreadLocal() throws Exception {
        TL_USER.set("Alice");

        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            var future = executor.submit(() -> {
                // ThreadLocal 值在 virtual thread 裏面,不會自動繼承
                System.out.println("[VirtualThread] USER = " + TL_USER.get());
                TL_USER.set("Cat");
                TL_USER.remove();
                return null;
            });
            future.get();
        }

        // 離開 VirtualThread 範圍後再取值 -> 還是存在,且已被修改
        System.out.println("[MainThread] USER = " + TL_USER.get());
        TL_USER.remove();
    }

    private static void testThreadLocal2() throws Exception {
        TL_USER.set("Alice");

        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            // 讀取父 ThreadLocal
            String parentValue = TL_USER.get();

            // 手動傳遞給虛擬執行緒
            var future = executor.submit(() -> {
                TL_USER.set(parentValue); // 設定子執行緒的值
                System.out.println("[Child VT] USER = " + TL_USER.get());
                TL_USER.set("Cat");
                TL_USER.remove();
                return null;
            });
            future.get();
        }

        System.out.println("[MainThread] USER = " + TL_USER.get());
        TL_USER.remove();
    }

    private static void testThreadLocal3() throws Exception {
        ITL_USER.set("Bob");

        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            var future = executor.submit(() -> {
                System.out.println("[Child VT] ITL_USER = " + ITL_USER.get());
                return null;
            });
            future.get();
        }

        System.out.println("[MainThread] ITL_USER = " + ITL_USER.get());
        TL_USER.remove();
    }

    private static void testScopedValue() throws Exception {
        ScopedValue.where(SV_USER, "Bob").run(() -> {
            System.out.println("[MainScope] USER = " + SV_USER.get());

            try (var scope = StructuredTaskScope.open()) {
                scope.fork(() -> {
                    System.out.println("[Child VT] USER = " + SV_USER.get());

                    return null;
                });

                scope.join();
            } catch (InterruptedException ie) {
                System.err.println("StructuredTaskScope: " + ie);
            }
        });

        // 離開 ScopedValue 範圍後再取值 -> 會丟例外
        try {
            System.out.println("[After Scope] USER = " + SV_USER.get());
        } catch (Exception e) {
            System.out.println("[After Scope] 無法取得,因為作用域已結束: " + e);
        }
    }
}

編譯,要加上參數

javac --enable-preview --release 25 ScopedValueVsThreadLocalDemo.java

執行

java --enable-preview ScopedValueVsThreadLocalDemo

結果

=== ThreadLocal 測試 ===
[VirtualThread] USER = null
[MainThread] USER = Alice

=== ThreadLocal 測試2 ===
[Child VT] USER = Alice
[MainThread] USER = Alice

=== InheritableThreadLocal 測試3 ===
[Child VT] ITL_USER = Bob
[MainThread] ITL_USER = Bob

=== ScopedValue 測試 ===
[MainScope] USER = Bob
[Child VT] USER = Bob
[After Scope] 無法取得,因為作用域已結束: java.util.NoSuchElementException: ScopedValue not bound