2026/4/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

沒有留言:

張貼留言