Java 的垃圾收集機制雖然讓開發者免於手動管理記憶體,但也帶來了一個挑戰:如何準確測量單個物件佔用的堆內存大小?由於 JVM 將內存管理封裝起來,直接調用 Runtime.freeMemory() 前後相減的做法並不可靠,因為 GC 的非確定性行為會導致數值波動。
許多開發者嘗試在建立物件前後呼叫 Runtime.getRuntime().freeMemory() 計算差值,但這忽略了 JVM 可能在兩次呼叫之間觸發 GC,導致可用內存突然減少,進而高估物件大小。同樣,若 GC 發生在物件創建後、第二次測量前,則會低估佔用量。
Java 專家 Tony Sintes 提出改用 Runtime.getRuntime().totalMemory() 計算兩次差值。其核心邏輯是:先強制 GC、記錄總堆大小,批量創建大量同類物件,再次強制 GC 後記錄總堆大小,兩者之差除以物件數量即為單個物件平均大小。示例代碼如下:
public class Memory {
private final static int SIZE = 500;
public static void main(String[] args) throws Exception {
Object[] array = new Object[SIZE];
Runtime.getRuntime().gc();
long start = Runtime.getRuntime().totalMemory();
for (int i = 0; i < SIZE; i++) {
array[i] = new Object();
}
Runtime.getRuntime().gc();
long end = Runtime.getRuntime().totalMemory();
System.out.println((start - end) / SIZE + " bytes per object");
}
}
上述方法仍有一隱患:單次 gc() 呼叫不保證垃圾收集徹底完成。為讓堆內存達到穩定狀態,需反覆執行 GC 直到已用內存不再顯著下降。以下工具方法可有效解決此問題:
private static final Runtime RUNTIME = Runtime.getRuntime();
private static long usedMemory() {
return RUNTIME.totalMemory() - RUNTIME.freeMemory();
}
private static void runGC() throws Exception {
long usedMem1 = usedMemory(), usedMem2 = Long.MAX_VALUE;
for (int i = 0; (usedMem1 < usedMem2) && (i < 500); ++i) {
RUNTIME.runFinalization();
RUNTIME.gc();
Thread.currentThread().yield();
usedMem2 = usedMem1;
usedMem1 = usedMemory();
}
}
此循環最多執行 500 次,每次執行完 GC 後讓出 CPU 時間片,直到已用內存連續兩次測量不再減少,即視為 GC 基本完成。配合此方法,totalMemory() 差值法才能得到可信的物件大小估算。
-Xms -Xmx 固定堆大小,減少擴容干擾。Instrumentation.getObjectSize()) 或 JOL (Java Object Layout) 庫。掌握這些技巧,能幫助你在性能調優、記憶體洩漏排查中更科學地評估物件佔用情況。