垃圾回收(GC)是 Java 自動內存管理的核心。本文系統梳理 GC 意義、六大經典算法、System.gc()/finalize() 機制、主 GC 觸發條件、減少 GC 開銷七大措施、運行參數觀測,並附帶經典演示代碼,構建完整 GC 知識體系。
| 優勢 | 劣勢 |
|---|---|
| 消除手動內存管理,大幅提升開發效率 | GC 過程佔用 CPU、產生停頓(Stop-the-World) |
| 防止內存洩漏、懸空指針,保障程序穩健性 | 早期算法無法 100% 回收廢棄內存 |
| 碎片整理優化內存佈局,提升分配速度 | 調優複雜,需權衡吞吐量、延遲、內存佔用 |
| 算法 | 核心思想 | 優點 | 缺點 | 代表實現 |
|---|---|---|---|---|
| 引用計數 | 對象頭計數器,引用 +1,失效 -1,歸零回收 | 即時回收,無長停頓,適合實時系統 | 無法處理循環引用;每次賦值增減開銷大 | 早期 Python、COM、Objective-C (ARC 改良) |
| 標記-清除 | 從根集掃描可達對象標記,掃描堆清除不可達 | 解決循環引用;實現簡單 | 產生內存碎片;兩次全堆掃描 | 早期 JVM、Mark-Sweep 收集器 |
| 標記-整理 | 標記後將存活對象向一端移動,更新引用 | 無碎片;分配指針碰撞極快 | 移動對象開銷大;需更新所有引用 | Serial Old、Parallel Old (CMS 整理階段) |
| 複製 | 堆分對象區+空閒區,GC 時將存活對象複製到空閒區 | 無碎片;分配極快(指針碰撞) | 浪費 50% 空間;存活率高時複製開銷大 | 新生代 Serial/Parallel Scavenge、G1 年輕代 |
| 分代收集 | 弱分代假設:多數對象朝生夕死。堆分新生代/老年代,不同代用不同算法 | 針對性優化:新生代用複製,老年代用標記-整理 | 實現複雜;跨代引用需記憶集/卡表 | HotSpot 所有商用收集器 |
| 自適應收集 | 運行時監控堆狀態,動態選擇最優算法/參數 | 無需人工調優;自適應負載變化 | 決策邏輯複雜;可能誤判 | Parallel Scavenge (PSAdaptiveSizePolicy)、G1、ZGC |
System.gc():建議 JVM 執行 Full GC,非強制,非即時。-verbose:gc:啟用 GC 日誌輸出。
[Full GC 168K->97K(1984K), 0.0253873 secs]
解讀:GC 前堆佔用 168K → 後 97K,堆總 1984K,回收 71K,耗時 25ms。
finalize() 做清理(釋放 native 內存、關閉文件句柄等)。finalize() 只用於釋放非堆資源,不用於釋放堆內存(GC 自動處理)。try-with-resources、Cleaner (JDK 9+)、PhantomReference 管理資源釋放。OutOfMemoryError。不確定性:觸發時機依賴 JVM 運行時決策,長期運行應用主 GC 必反覆發生。
| 措施 | 原理 | 代碼示例 |
|---|---|---|
| 1. 不顯式調用 System.gc() | 避免觸發不必要的 Full GC,增加停頓頻率 | // 刪除 System.gc() 調用 |
| 2. 減少臨時對象創建 | 降低新生代分配速率,延長 Minor GC 間隔 | 循環外複用對象、用基本類型替代包裝類 |
| 3. 對象不用盡早置 null | 幫助 GC 識別垃圾,提高回收效率 | list = null; // 退出作用域前 |
| 4. 用 StringBuilder 替代 String 拼接 | 避免產生大量中間 String 對象 | new StringBuilder().append(a).append(b).toString() |
| 5. 能用基本類型不用包裝類 | 基本類型佔用內存少得多,無對象頭開銷 | int i = 0; // 而非 Integer i = 0; |
| 6. 少用靜態對象變量 | 靜態變量生命週期長,難被 GC 回收 | // 避免 static List |
| 7. 分散對象創建/刪除時間 | 避免瞬間大量分配觸發 Full GC 或大量垃圾導致下次分配觸發 GC | 批量處理改為流式/分批處理 |
class Chair {
static boolean gcrun = false, f = false;
static int created = 0, finalized = 0;
int i;
Chair() { i = ++created; if (created == 47) System.out.println("Created 47"); }
protected void finalize() {
if (!gcrun) { gcrun = true; System.out.println("Beginning to finalize after " + created + " Chairs"); }
if (i == 47) { System.out.println("Finalizing Chair #47, stop creation"); f = true; }
finalized++; if (finalized >= created) System.out.println("All " + finalized + " finalized");
}
}
public class Garbage {
public static void main(String[] args) {
while (!Chair.f) { new Chair(); new String("To take up space"); }
System.out.println("After all: created=" + Chair.created + ", finalized=" + Chair.finalized);
if ("before".equals(args[0])) { System.gc(); System.runFinalization(); }
System.out.println("bye!");
if ("after".equals(args[0])) System.runFinalizersOnExit(true); // 已棄用
}
}
輸出展示:對象創建 → GC 啟動 → finalize 執行順序 → 程序結束前最終清理。
| 收集器 | 適用場景 | JDK 版本 |
|---|---|---|
| Serial | 單核、小內存、Client 模式 | 所有 |
| Parallel Scavenge/Old | 多核、高吞吐、批處理、無低延遲要求 | 6+ |
| CMS | 多核、低延遲、老年代併發標記(已棄用移除) | 9-13 棄用,14 移除 |
| G1 | 大堆 (4G+)、可預測停頓、混合收集、區域化 | 7u4+ (9 默認) |
| ZGC | 超大堆 (TB 級)、亞毫秒停頓、併發整理 | 11 實驗,15 生產 |
| Shenandoah | 大堆、低延遲、併發整理、紅黑樹區域管理 | 12 實驗,15 生產 |
System.gc() 不可靠,finalize() 危險,編碼規範(措施 1-7)是減少 GC 壓力的根本。-Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=20m (JDK 9+)。垃圾回收雖自動,但「自動 ≠ 無需關注」。深入理解 GC 原理、參數、日誌分析,才能在高併發、大數據、低延遲場景下駕馭 JVM 內存,避免 OOM、長停頓等生產事故。