Java 堆是執行時期資料區,所有物件實例均在此分配空間。JVM 透過垃圾回收器自動管理記憶體,釋放不再被引用的物件空間,並可整理記憶體碎片。本文深度剖析 GC 意義、核心演算法、監控工具與最佳實務。
所有 GC 演算法需完成兩件事:發現無用物件、回收其佔用記憶體。多數演算法採用根集合概念——程式可直接訪問的引用變數集合(區域變數、參數、靜態欄位)。
| 演算法 | 核心思想 | 優點 | 缺點 |
|---|---|---|---|
| 引用計數法 | 每物件維護計數器,歸零即回收 | 即時性好,無長時間停頓 | 無法處理循環引用;每次賦值需增減計數,開銷大 |
| 標記-清除 | 從根集合掃描標記可達物件,清除不可達者 | 解決循環引用 | 產生記憶體碎片;需暫停應用執行 |
| 壓縮演算法 | 清除後將存活物件移至堆一端,更新引用 | 消除碎片 | 移動物件與更新引用成本高;常引入控制代碼間接訪問 |
| 複製演算法 | 將堆分為物件區與空閒區,GC 時複製存活物件到空閒區 | 無碎片;分配極快 | 堆利用率僅 50%;需暫停應用 |
| 分代收集 | 依物件壽命分代,年輕代頻繁 GC,老年代較少 | 符合「多數物件朝生夕死」規律,效能最佳 | 實作複雜 |
| 自適應演算法 | 監控堆狀態動態選擇最適演算法 | 自動優化 | 決策邏輯複雜 |
java -verbose:gc TestGC
// 輸出範例:[Full GC 168K->97K(1984K), 0.0253873 secs]
// 箭頭前後為 GC 前後存活物件大小,括號內為堆總大小
protected void finalize() throws Throwablefree())class Chair {
static boolean gcRun = false, stop = false;
static int created = 0, finalized = 0;
int id;
Chair() { id = ++created; }
protected void finalize() {
if (!gcRun) { gcRun = true; System.out.println("GC started"); }
if (id == 47) { stop = true; System.out.println("Finalizing #47"); }
if (++finalized >= created) System.out.println("All finalized");
}
}
public class GCDemo {
public static void main(String[] args) {
while (!Chair.stop) { new Chair(); new String("padding"); }
System.out.println("Created: " + Chair.created + ", Finalized: " + Chair.finalized);
if ("before".equals(args[0])) { System.gc(); System.runFinalization(); }
else if ("after".equals(args[0])) System.runFinalizersOnExit(true);
}
}
| 特性 | 說明 | 應對策略 |
|---|---|---|
| 發生時間不可預知 | 視演算法、JVM 實作而定 | 勿假設何時發生;方法結束的區域物件即成垃圾 |
| 精確性要求高 | 必須精確標記存活物件與引用關係 | 防止記憶體洩漏;支援物件移動與複製 |
| 多種收集器可選 | 串行、並行、並發、增量等 | 一般用預設;即時系統用增量;多核大記憶體用並行 |
| 與 JVM 記憶體模型緊密相關 | HotSpot 採用先進 OO 框架支援多種 GC | 了解目標 JVM 的記憶體結構 |
| 可調參數豐富 | 現代 GC 提供多維度調優 | 根據應用場景調優以獲最佳效能 |
System.gc() 僅為建議,不保證執行null理解 GC 機制有助於編寫高效能、穩定的 Java 應用。雖日常開發可不直接操作堆,但深入掌握其原理能在關鍵時刻做出正確架構決策。