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

2026/03/23

Structure Concurrency

是一種讓多執行緒邏輯變得更容易理解與管理的程式設計模型,目標是讓「並行任務的生命週期」像區塊結構(block structure)一樣有明確的範圍。

  • 子任務(threads, tasks)都必須在離開某個區塊前結束

  • 執行緒之間的層級關係(parent/child)是語法上明確可見的。

  • 不會有「孤兒執行緒」(dangling thread)偷偷跑在背景。

傳統的 thread

  • 任務彼此獨立、缺乏邏輯關聯。

  • 錯誤傳遞困難(子執行緒例外不會自動向上傳遞)。

  • 難以在結束前確保所有子任務完成。

void process() {
    Thread t = new Thread(() -> downloadFile());
    t.start();
    // ... do other work ...
    // 忘記 join() 或沒有捕捉例外,就可能造成 thread 泄漏
}

Structure Concurrency (JEP 453 / Java 21)

  • ShutdownOnFailure: 若任一子任務失敗,其他會自動中止。

  • ShutdownOnSuccess: 若有一個成功,其餘終止(常用於競賽式查詢)。

  • 範圍清晰、錯誤可控、資源可預期。

import java.util.concurrent.*;

void process() throws Exception {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Future<String> f1 = scope.fork(() -> downloadFile());
        Future<String> f2 = scope.fork(() -> fetchMetadata());
        scope.join();              // 等待所有子任務
        scope.throwIfFailed();     // 傳遞例外
        System.out.println(f1.resultNow() + f2.resultNow());
    } // scope 區塊結束時,自動關閉所有未完成子任務
}
  • Virtual Threads 解決「效能 & 可擴充性」。

  • Structured Concurrency 解決「邏輯一致性 & 錯誤處理」。

Example

import java.util.concurrent.*;
import java.lang.management.*;

public class StructuredConcurrencyVirtualThreadsExample {

    public static void main(String[] args) throws Exception {
        System.out.println("Java: " + System.getProperty("java.version"));
        System.out.println();

        System.out.println("=== Example 1: Structured Concurrency (ShutdownOnFailure) ===");
        example();

        System.out.println();
        System.out.println("=== Example 2: Virtual vs Native Thread Performance Test ===");
        compareVirtualAndNativeThreads();
    }

    static void example() {
        // 使用 StructuredTaskScope.open() 創建作用域
        try (var scope = StructuredTaskScope.open()) {
            // 創建三個子任務
            var f1 = scope.fork(() -> simulatedIo("Service-A", 1200, false));
            var f2 = scope.fork(() -> simulatedIo("Service-B", 2500, true));
            var f3 = scope.fork(() -> simulatedIo("Service-C", 3000, false));

            // 等待所有子任務完成
            scope.join();

            // 處理結果
            System.out.println("Results: " +
                (f1.state() == StructuredTaskScope.Subtask.State.SUCCESS ? f1.get() : "Failed") + ", " +
                (f2.state() == StructuredTaskScope.Subtask.State.SUCCESS ? f2.get() : "Failed") + ", " +
                (f3.state() == StructuredTaskScope.Subtask.State.SUCCESS ? f3.get() : "Failed"));

        } catch (Exception e) {
            System.out.println("Scope finished with failure: " + e);
        }
    }

    /**
     * Compare Virtual Threads vs Native Threads with ASCII table output.
     */
    static void compareVirtualAndNativeThreads() throws Exception {
        int taskCount = 10_000;
        System.out.printf("Launching %,d simulated I/O tasks...\n", taskCount);

        long nativeTime = measureThreadTypePerformance(taskCount, false);
        long virtualTime = measureThreadTypePerformance(taskCount, true);

        long usedMem = getUsedMemoryMB();

        // ASCII Table output
        System.out.println();
        System.out.println("+--------------------+----------------+----------------+");
        System.out.println("| Thread Type        | Time (ms)      | Observations   |");
        System.out.println("+--------------------+----------------+----------------+");
        System.out.printf ("| %-18s | %-14d | %-14s |%n", "Native Threads", nativeTime, "limited pool");
        System.out.printf ("| %-18s | %-14d | %-14s |%n", "Virtual Threads", virtualTime, "scales easily");
        System.out.println("+--------------------+----------------+----------------+");
        System.out.printf ("| %-18s | %-14d | %-14s |%n", "Heap Used (MB)", usedMem, "after test");
        System.out.println("+--------------------+----------------+----------------+");
    }

    static long measureThreadTypePerformance(int count, boolean virtual) throws Exception {
        long start = System.currentTimeMillis();

        ExecutorService executor = virtual ? Executors.newVirtualThreadPerTaskExecutor()
                                           : Executors.newFixedThreadPool(200);

        try (executor) {
            for (int i = 0; i < count; i++) {
                int id = i;
                executor.submit(() -> {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException ignored) {}
                    return null;
                });
            }
        }

        long end = System.currentTimeMillis();
        return end - start;
    }

    static String simulatedIo(String name, long millis, boolean fail) throws InterruptedException {
        System.out.printf("[%s] running on %s, sleep %dms%n", name, Thread.currentThread(), millis);
        Thread.sleep(millis);
        if (fail) throw new RuntimeException(name + " failed!");
        return name + "-done";
    }

    static long getUsedMemoryMB() {
        MemoryMXBean mbean = ManagementFactory.getMemoryMXBean();
        long used = mbean.getHeapMemoryUsage().getUsed();
        return used / (1024 * 1024);
    }
}

編譯與執行

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

執行結果

Java: 25

=== Example 1: Structured Concurrency (ShutdownOnFailure) ===
[Service-A] running on VirtualThread[#26]/runnable@ForkJoinPool-1-worker-1, sleep 1200ms
[Service-C] running on VirtualThread[#30]/runnable@ForkJoinPool-1-worker-2, sleep 3000ms
[Service-B] running on VirtualThread[#28]/runnable@ForkJoinPool-1-worker-4, sleep 2500ms
Scope finished with failure: java.util.concurrent.StructuredTaskScope$FailedException: java.lang.RuntimeException: Service-B failed!

=== Example 2: Virtual vs Native Thread Performance Test ===
Launching 10,000 simulated I/O tasks...

+--------------------+----------------+----------------+
| Thread Type        | Time (ms)      | Observations   |
+--------------------+----------------+----------------+
| Native Threads     | 10232          | limited pool   |
| Virtual Threads    | 240            | scales easily  |
+--------------------+----------------+----------------+
| Heap Used (MB)     | 61             | after test     |
+--------------------+----------------+----------------+

同時支援舊的 Native Thread

  • 自動 join 或取消子任務;

  • 錯誤可統一處理;

  • 可以使用任意 Executor 來決定底層執行緒類型。

    static void example2() {
        try (var scope = StructuredTaskScope.open()) {

            // Virtual Thread Executor
            ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();

            // fork 只能傳 Callable,不傳 Executor
            Callable<String> task1 = () -> simulatedIo("VT-A", 500, false);
            Callable<String> task2 = () -> simulatedIo("VT-B", 800, false);

            var f1 = scope.fork(task1);
            var f2 = scope.fork(task2);

            // Native Thread Executor
            ExecutorService nativeExecutor = Executors.newFixedThreadPool(2);
            Callable<String> cpuTask = () -> heavyComputation("NT-A");
            var f3 = scope.fork(cpuTask);

            scope.join(); // 等待所有子任務完成

            System.out.println("Results: " + f1.get() + ", " + f2.get() + ", " + f3.get());

            virtualExecutor.shutdown();
        } catch (Exception e) {
            System.out.println("Scope finished with failure: " + e);
        }

    }

    // 模擬 CPU 任務
    static String heavyComputation(String name) {
        System.out.printf("[%s] CPU task on %s%n", name, Thread.currentThread());
        long sum = 0;
        for (long i = 0; i < 10_000_000L; i++) sum += i;
        return name + "-done";
    }