首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

深入解析 Java 物件記憶體佈局:使用 sun.misc.Unsafe 進行記憶體內省

     編輯:Java 相關知識

對於資深 Java 開發者而言,精確掌握物件在堆中的記憶體佔用情況,是進行效能調優與排查 OOM 問題的基石。雖然現代伺服器動輒配備 64GB 甚至更大的記憶體,但在雲端部署成本敏感、或是單機多租戶的場景下,記憶體壓力依然普遍存在。與其盲目升級硬體,不如在開發早期引入輕量級的記憶體分析手段,快速定佔用大戶。

核心工具:sun.misc.Unsafe

Oracle JDK 內建的 sun.misc.Unsafe 提供了底層記憶體操作能力,我們可以利用其以下 Native 方法構建純 Java 的物件內省器:

  • objectFieldOffset(Field f):獲取非靜態欄位在物件中的位元組偏移量
  • arrayBaseOffset(Class<?> arrayClass):獲取陣列首元素偏移量
  • arrayIndexScale(Class<?> arrayClass):獲取陣列單元素大小
  • addressSize():獲取 JVM 位址大小(32/64 位)

註:staticFieldBase 與 staticFieldOffset 用於靜態欄位操作,本文不涉及。

物件記憶體佈局推導邏輯

  1. 遞歸收集欄位:透過 Class.getDeclaredFields() 遍歷當前類別及所有父類別,獲取完整欄位列表。
  2. 計算非靜態欄位 Shallow Size:基本類型按預設大小計算;物件引用大小為 4 或 8 位元組(取決於壓縮指標開啟與否),安全獲取方式為 unsafe.arrayIndexScale(Object[].class)。
  3. 陣列 Shallow Size 計算:baseOffset + indexScale * length,另加陣列引用本身大小。
  4. 處理循環引用:使用 IdentityHashMap 記錄已訪問物件,避免無限遞歸。

壓縮指標(Compressed Oops)機制詳解

在 32GB 以下堆中,預設開啟 -XX:+UseCompressedOops,物件引用壓縮為 4 位元組。JVM 假設物件按 8 位元組對齊,故低 3 位恆為 0,實際存儲 (address >> 3),使 32 位值可寻址 35 位空間(32GB)。超過 32GB 或關閉壓縮指標時,引用固定為 8 位元組。

實戰避坑指南

  • 列印陣列內容務必使用 Arrays.toString(),支援基本型別與物件陣列。
  • 基本型別會自動裝箱傳入內省方法,需防範 Integer.value 等欄位導致的無限遞歸。
  • 物件陣列非空元素的內省,本質上是物件圖中額外的一層間接引用。

ClassIntrospector 使用範例

ClassIntrospector ci = new ClassIntrospector();
ObjectInfo info = ci.introspect("0123456789012345678901234567"); // 28 字元字串
System.out.println("Deep Size: " + info.getDeepSize());
System.out.println(info); // 列印完整記憶體佈局樹

輸出將包含每個欄位的偏移量、大小、類型及巢狀子物件資訊。注意:ClassIntrospector 非執行緒安全,但可在同一執行緒重複呼叫。

完整原始碼附錄

ClassIntrospector.java

import sun.misc.Unsafe;
import java.lang.reflect.*;
import java.math.BigDecimal;
import java.util.*;

public class ClassIntrospector {
    public static void main(String[] args) throws IllegalAccessException {
        ClassIntrospector ci = new ClassIntrospector();
        Map<String, BigDecimal> map = new HashMap<>(10);
        map.put("one", BigDecimal.ONE);
        map.put("zero", BigDecimal.ZERO);
        map.put("ten", BigDecimal.TEN);
        ObjectInfo res = ci.introspect("0123456789012345678901234567");
        System.out.println(res.getDeepSize());
        System.out.println(res);
    }
    // ...(完整實現見原文,含 TestObj、TestObjChild、ObjectInfo 等內部類別)
    // 關鍵邏輯:getAllFields 遞歸收集父類欄位、introspect 遞歸構建 ObjectInfo 樹、
    // getShallowSize 區分基本型別與引用大小、getContents 格式化陣列輸出
}

ObjectInfo.java

import java.util.*;

public class ObjectInfo {
    public final String name, type, contents;
    public final int offset, length, arrayBase, arrayElementSize, arraySize;
    public final List<ObjectInfo> children = new ArrayList<>(1);

    public ObjectInfo(String name, String type, String contents, int offset,
                      int length, int arraySize, int arrayBase, int arrayElementSize) {
        this.name = name; this.type = type; this.contents = contents;
        this.offset = offset; this.length = length;
        this.arraySize = arraySize; this.arrayBase = arrayBase;
        this.arrayElementSize = arrayElementSize;
    }
    public void addChild(ObjectInfo info) { if (info != null) children.add(info); }

    public long getDeepSize() {
        return length + arraySize + getUnderlyingSize(arraySize != 0);
    }
    private long getUnderlyingSize(boolean isArray) {
        long size = 0;
        for (ObjectInfo child : children)
            size += child.arraySize + child.getUnderlyingSize(child.arraySize != 0);
        if (!isArray && !children.isEmpty())
            size += children.get(children.size()-1).offset + children.get(children.size()-1).length;
        return size;
    }
    public void sort() { children.sort(Comparator.comparingInt(o -> o.offset)); }
    @Override public String toString() { /* 縮排列印邏輯 */ return ""; }
}

原文出處:java-performance.info

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved