垃圾收集技術詳解筆記
1. 分代收集理論
當前虛擬機的垃圾收集采用分代收集算法,根據對象存活周期將內存分為不同代區,以優化回收效率。
- 核心分區:
- 新生代(Young Generation):對象存活周期短,約99%對象在每次回收時死亡。
- 老年代(Old Generation):對象存活率高,無額外分配擔保空間。
- 算法選擇:
- 新生代:適用復制算法(效率高,只需復制少量存活對象)。
- 老年代:適用標記-清除或標記-整理算法(避免復制開銷,但速度慢10倍以上)。
2. 垃圾收集算法詳解
2.1 標記-復制算法
解決效率問題,將內存分為大小相等的兩塊。
- 工作流程:
- 使用其中一塊內存。
- 當內存耗盡,將存活對象復制到另一塊。
- 清理原內存塊。
- 內存變化示例:
- 整理前:碎片化狀態。
- 整理后:存活對象集中到另一塊,內存連續。
- 內存區域類型:
- 可用內存、可回收內存、存活對象、保留內存。
2.2 標記-清除算法
最基礎算法,分“標記”和“清除”兩階段。
- 工作流程:
- 標記存活對象(或需回收對象)。
- 清除未標記對象。
- 缺點:
- 效率問題:標記大量對象時性能低。
- 空間問題:產生內存碎片(不連續空間)。
- 內存變化示例:
- 整理前:碎片化狀態。
- 整理后:碎片化更嚴重,僅區分可用內存、可回收內存、存活對象。
2.3 標記-整理算法
專為老年代設計,標記后移動對象以消除碎片。
- 工作流程:
- 標記存活對象。
- 將所有存活對象向一端移動。
- 清理邊界外內存。
- 內存變化示例:
- 回收前:碎片化狀態。
- 回收后:對象緊湊排列,區分存活對象、可回收內存、未使用內存。
3. 垃圾收集器實現
收集器是算法的具體實現,需根據場景選擇。無“萬能”收集器。
3.1 Serial收集器(-XX:+UseSerialGC, -XX:+UseSerialOldGC)
- 特點:
- 單線程收集器,工作時暫停所有線程(Stop The World)。
- 簡單高效(無線程交互開銷)。
- 算法:
- 新生代:復制算法。
- 老年代:標記-整理算法。
- 適用場景:客戶端模式或小內存應用。
3.2 Parallel Scavenge收集器(-XX:+UseParallelGC, -XX:+UseParallelOldGC)
- 特點:
- Serial的多線程版本(默認線程數 = CPU核數)。
- 關注吞吐量(CPU運行用戶代碼時間占比)。
- 算法:
- 新生代:復制算法。
- 老年代:標記-整理算法。
- Parallel Old收集器:
- Parallel Scavenge的老年代版本,多線程 + 標記-整理算法。
- JDK8默認收集器,適合高吞吐場景。
3.3 ParNew收集器(-XX:+UseParNewGC)
- 特點:
- 類似Parallel,但專為與CMS收集器配合設計。
- 新生代使用復制算法。
- 適用場景:Server模式下的首選(與CMS兼容)。
3.4 CMS收集器(-XX:+UseConcMarkSweepGC)
- 特點:
- 并發收集器(最短停頓時間),用戶線程與GC線程并行。
- 基于標記-清除算法。
- 工作流程:
- 初始標記(STW):標記GC Roots直接引用對象。
- 并發標記:遍歷對象圖(無停頓)。
- 重新標記(STW):修正并發標記變動(用增量更新算法)。
- 并發清理:清除未標記對象。
- 并發重置:重置標記數據。
- 缺點:
- CPU敏感(與業務線程搶資源)。
- 浮動垃圾(并發階段新垃圾需下次回收)。
- 內存碎片(需定期整理)。
- 不確定性(可能觸發Concurrent Mode Failure,降級為Serial Old)。
- 關鍵參數:
-XX:+UseConcMarkSweepGC # 啟用CMS -XX:ConcGCThreads # 并發GC線程數 -XX:+UseCMSCompactAtFullCollection # FullGC后整理碎片 -XX:CMSFullGCsBeforeCompaction # 多少次FullGC后整理一次(默認0) -XX:CMSInitiatingOccupancyFraction # 老年代使用比例觸發FullGC(默認92%) -XX:+CMSScavengeBeforeRemark # CMS前啟動Minor GC
4. 億級流量電商系統JVM優化案例
針對訂單系統(8G內存,分配4G給JVM)。
- 優化目標:減少Full GC(避免對象過早進入老年代)。
- 參數配置:
-Xms3072M -Xmx3072M -Xmn2048M # 堆大小(新生代2G) -Xss1M # 線程棧大小 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M # 元空間 -XX:SurvivorRatio=8 # Eden與Survivor比例 -XX:MaxTenuringThreshold=5 # 對象年齡閾值(從15改為5) -XX:PretenureSizeThreshold=1M # 直接進入老年代對象大小閾值 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC # 新生代ParNew + 老年代CMS -XX:CMSInitiatingOccupancyFraction=92 # 老年代92%觸發FullGC -XX:+UseCMSCompactAtFullCollection # FullGC后整理碎片 -XX:CMSFullGCsBeforeCompaction=3 # 每3次FullGC整理一次
- 優化原理:
- 增大新生代(-Xmn2048M),減少對象動態年齡判斷導致的過早晉升。
- 降低對象年齡閾值(-XX:MaxTenuringThreshold=5),確保短期對象在Minor GC回收。
- CMS默認參數適合高峰后Full GC(幾小時一次)。
5. 垃圾收集底層算法
5.1 三色標記算法
解決并發標記中的漏標問題,將對象分為三色:
- 黑色:已掃描完(安全存活)。
- 灰色:已掃描,但引用未全掃描。
- 白色:未掃描(不可達對象)。
- 問題與解決:
- 多標(浮動垃圾):并發階段局部變量銷毀導致本應回收的對象未被回收(下一輪GC處理)。
- 漏標:通過讀寫屏障解決:
- 增量更新(CMS使用):黑色對象插入新引用時記錄,重新掃描。
- 原始快照(SATB, G1使用):灰色對象刪除引用時記錄,重新掃描。
- 讀寫屏障實現:
// 寫屏障示例(增量更新) void oop_field_store(oop* field, oop new_value) {pre_write_barrier(field); // 寫前操作(記錄舊值)*field = new_value;post_write_barrier(field, new_value); // 寫后操作(記錄新值) }
5.2 記憶集與卡表
解決跨代引用問題(如新生代引用老年代)。
- 記憶集(Remember Set):記錄跨代指針集合。
- 卡表實現:字節數組(CARD_TABLE[]),每卡頁512字節。
- 卡頁變臟(=1):有跨代指針時更新。
- 維護:通過寫屏障自動更新卡表狀態。
6. 總結
- 分代收集是JVM垃圾回收核心,新生代和老年代需匹配不同算法。
- 收集器選擇需權衡吞吐量(Parallel Scavenge)和停頓時間(CMS)。
- 優化案例顯示:合理配置新生代大小、對象年齡閾值及CMS參數可顯著減少Full GC。
- 底層算法(三色標記、讀寫屏障)確保并發標記的正確性,避免漏標。