2026/05/04

Java Atomic Variables

java.util.concurrent.atomic 定義了對單一變數進行 atomic operations 的類別,所有類別都有 get 與 set methods,可讀寫 volatile variables。set 可確定會在 get 的前面先處理。

對於物件管理,如果有兩個 thread 同時做 get/set,會導致資料異常,所以通常會在 set method 加上 synchronized 來限制一次只有一個 thread 進入該 method

例如

Counter 類別的變數 c,會因為多個 thread 同時使用而異常

class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}

所以會改寫為

class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }

}

synchronized 可解決 multi-thread 問題,但會產生效能問題。

以 AtomicInteger 改寫

import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
    private AtomicInteger c = new AtomicInteger(0);

    public void increment() {
        c.incrementAndGet();
    }

    public void decrement() {
        c.decrementAndGet();
    }

    public int value() {
        return c.get();
    }

}

Atomic Operation

non-blocking 演算法提供了 compare-and-swap CAS 方法,可確保資料完整性。

CAS operation 使用以下三個 operands

  1. M 要操作 operate 的記憶體位置

  2. A 該變數期待的既有原始數值

  3. B 需要被設定的新數值

CAS operation 可自動 atomically 從 M 到 B 更新數值,只會在 M 確認為 A 時,更新為 B

因此可不使用 synchronization lock,完成資料的更新,沒有 thread 會被 suspended,也不需要 context switching。


常用的 Atomic Variables 有 AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference,分別代表 int, long, boolean, object reference 這些數值可自動被安全地更新,主要 methods 有

  • get()

    等同於讀取 volatile variable,可直接取得其他 thread 更新的數值

  • incrementAndGet()

  • set()

    等同於寫入 volatile variable

  • lazySet()

    最終會寫入變數,但不保證可立即被其他 thread 取得。速度比 set 更快。可用在 cache,或被 gc 的資料

  • compareAndSet()

    成功就回傳 true

  • weakCompareAndSetPlain(), weakCompareAndSetVolatile()

    weakCompareAndSet() 已被 deprecated

References

# Atomic Variables Java Tutorial

An Introduction to Atomic Variables in Java | Baeldung

java.util.concurrent.atomic (Java SE 25 & JDK 25)

Atomic Variables in Java with Examples - GeeksforGeeks

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