對於資深 Java 開發者而言,精確掌握物件在堆中的記憶體佔用情況,是進行效能調優與排查 OOM 問題的基石。雖然現代伺服器動輒配備 64GB 甚至更大的記憶體,但在雲端部署成本敏感、或是單機多租戶的場景下,記憶體壓力依然普遍存在。與其盲目升級硬體,不如在開發早期引入輕量級的記憶體分析手段,快速定佔用大戶。
Oracle JDK 內建的 sun.misc.Unsafe 提供了底層記憶體操作能力,我們可以利用其以下 Native 方法構建純 Java 的物件內省器:
objectFieldOffset(Field f):獲取非靜態欄位在物件中的位元組偏移量arrayBaseOffset(Class<?> arrayClass):獲取陣列首元素偏移量arrayIndexScale(Class<?> arrayClass):獲取陣列單元素大小addressSize():獲取 JVM 位址大小(32/64 位)註:staticFieldBase 與 staticFieldOffset 用於靜態欄位操作,本文不涉及。
Class.getDeclaredFields() 遍歷當前類別及所有父類別,獲取完整欄位列表。unsafe.arrayIndexScale(Object[].class)。baseOffset + indexScale * length,另加陣列引用本身大小。IdentityHashMap 記錄已訪問物件,避免無限遞歸。在 32GB 以下堆中,預設開啟 -XX:+UseCompressedOops,物件引用壓縮為 4 位元組。JVM 假設物件按 8 位元組對齊,故低 3 位恆為 0,實際存儲 (address >> 3),使 32 位值可寻址 35 位空間(32GB)。超過 32GB 或關閉壓縮指標時,引用固定為 8 位元組。
Arrays.toString(),支援基本型別與物件陣列。Integer.value 等欄位導致的無限遞歸。ClassIntrospector ci = new ClassIntrospector();
ObjectInfo info = ci.introspect("0123456789012345678901234567"); // 28 字元字串
System.out.println("Deep Size: " + info.getDeepSize());
System.out.println(info); // 列印完整記憶體佈局樹
輸出將包含每個欄位的偏移量、大小、類型及巢狀子物件資訊。注意:ClassIntrospector 非執行緒安全,但可在同一執行緒重複呼叫。
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 格式化陣列輸出
}
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 ""; }
}