垃圾回收算法
標記-復制
缺點:內存利用率低,有一塊區域無法使用。
標記-清除
缺點:
1. 效率問題 (如果需要標記的對象太多,效率不高)
2. 空間問題(標記清除后會產生大量不連續的碎片)
標記-整理
分代收集
????????根據對象存活周期的不同將內存分為幾塊。一般將java堆分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。
????????當前虛擬機的垃圾收集都采用分代收集算法。
垃圾回收器
新生代和老年代使用的垃圾收集器的組合:
Serial收集器
(-XX:+UseSerialGC -XX:+UseSerialOldGC)
特點:
- 新生代采用復制算法,老年代采用標記-整理算法。
- "Stop The World"
- 簡單而高效(與其他收集器的單線程相比)
Serial Old收集器是Serial收集器的老年代版本,使用場景:
- 在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用
- 另一種用途是作為CMS收集器的后備方案
Parallel Scavenge收集器
(-XX:+UseParallelGC(年輕代),-XX:+UseParallelOldGC(老年代))
- 新生代采用復制算法,老年代采用標記-整理算法。Serial收集器的多線程版本。
- "Stop The World"
- 默認的收集線程數跟cpu核數相同,當然也可以用參數(-XX:ParallelGCThreads)指定收集線程數,但是一般不推薦修改
與其他垃圾收集器比較:
- Parallel收集器關注點是吞吐量(用戶線程的高效率的利用CPU)(縮短垃圾回收時間)。
- CMS等垃圾收集器的關注點更多的是用戶線程的停頓時間(提高用戶體驗)(stop the world時間)
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,,使用場景:
在注重吞吐量以及CPU資源的場合,都可以優先考慮 Parallel Scavenge收集器和Parallel Old收集器(JDK8默認的新生代和老年代收集器)。
ParNew收集器
(-XX:+UseParNewGC)
- 新生代采用復制算法
- 跟Parallel收集器很類似, 但是它只能用于新生代,和CMS收集器配合使用(Parallel收集器不能和CMS配合使用)
CMS收集器
(-XX:+UseConcMarkSweepGC(old))
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。它非常符合在注重用戶體驗的應用上使用,它是HotSpot虛擬機第一款真正意義上的并發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。基于“標記-清除”算法實現。
標記過程:
1. 初始標記:
stop the world,?并記錄下gc roots直接能引用的對象(根對象),速度很快。
2. 并發標記:
從GC Roots的直接關聯對象開始遍歷整個對象圖的過程, 這個過程耗時較長但是不需要停頓用戶線程。因為用戶程序繼續運行,可能會有導致已經標記過的對象狀態發生改變。出現多標注或者漏標注。
多標注會出現浮動垃圾,可以接受,可以在下一次的gc時回收掉; 漏標注很嚴重,未標注的會被垃圾回收,JVM不能回收還被引用著的對象。
3. 重新標記:
stop the world,修正并發標記期間因為用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄(主要是處理漏標問題),這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比并發標記階段時間短。主要用到三色標記里的 增量更新算法。
4. 并發清理:
開啟用戶線程,同時GC線程開始對未標記的區域做清掃。這個階段如果有新增對象會被標記為黑色不做任何處理。
5. 并發重置:
重置本次GC過程中的標記數據
CMS優點:并發收集、低停頓
CMS缺點:
- 對CPU資源敏感(會和服務(用戶線程)搶資源);
- 無法處理浮動垃圾(在并發標記和并發清理階段又產生垃圾,這種浮動垃圾只能等到下一次gc再清理了);
- “標記-清除”算法會導致收集結束時會有大量空間碎片產生,當然通過參數-XX:+UseCMSCompactAtFullCollection可以讓jvm在執行完標記清除后再做整理
- 執行過程中的不確定性。會存在上一次垃圾回收還沒執行完,然后垃圾回收又被觸發的情況,特別是在并發標記和并發清理階段會出現。因為是 一邊回收,系統一邊運行,也許沒回收完就再次觸發full gc,也就是"concurrent mode failure",此時會進入stop the?world,用serial old垃圾收集器來回收
CMS的相關核心參數
1. -XX:+UseConcMarkSweepGC:啟用cms
2. -XX:ConcGCThreads:并發的GC線程數
3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做壓縮整理(減少碎片)
4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后壓縮一次,默認是0,代表每次FullGC后都會壓縮一次
5. -XX:CMSInitiatingOccupancyFraction: 當老年代使用達到該比例時會觸發FullGC(默認是92,這是百分比)
6. -XX:+UseCMSInitiatingOccupancyOnly:只使用設定的回收閾值(-XX:CMSInitiatingOccupancyFraction設定的值),如果不指定,JVM僅在第一次使用設定值,后續則會自動調整
7. -XX:+CMSScavengeBeforeRemark:在CMS GC前啟動一次minor gc,降低CMS GC標記階段(也會對年輕代一起做標記,如果在minor gc就干掉了很多對垃圾對象,標記階段就會減少一些標記時間)時的開銷,一般CMS的GC耗時 80%都在標記階段
8. -XX:+CMSParallellnitialMarkEnabled:表示在初始標記的時候多線程執行,縮短STW
9. -XX:+CMSParallelRemarkEnabled:在重新標記的時候多線程執行,縮短STW;
JVM參數優化
億級流量電商系統如何優化JVM參數設置(ParNew+CMS)
對于8G內存,我們一般是分配4G內存給JVM,正常的JVM參數配置如下:
-Xms : 堆初始內存大小
-Xmx:最大堆內存大小
-Xmn :?新生代初始和最大值
-Xss :?每個線程的棧大小
-XX:MetaspaceSize :?初始元空間大小
-XX:MaxMetaspaceSize :?最大元空間大小
-XX:SurvivorRatio :?設置 Eden 區與 Survivor 區的比例
-Xms3072M -Xmx3072M
-Xss1M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:SurvivorRatio=8
這樣的配置, young?=1G??(eden = 819M?, survivor = 102M)? ?old = 2048M= 2G
每秒60M垃圾, 大約819?/ 60 = 14s占滿eden, 觸發minor gc
優化?: 修改JVM參數
-Xms3072M -Xmx3072M -Xmn2048M
-Xss1M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:SurvivorRatio=8
這樣的配置, young?=2G??(eden =1638M , survivor = 204M)? ?old = 1G
每秒60M垃圾, 大約1638?/ 60 = 27s占滿eden, 觸發minor gc
JVM優化手段:
1. 無非就是讓短期存活的對象盡量都留在survivor里,不要進入老年代,這樣在minor gc的時候這些對象都會被回收,不會進到老年代從而導致full gc。
2.?對象年齡應該為多少才移動到老年代比較合適:大多數對象一般在幾秒內就會變為垃圾,完全可以將默認的15歲改小一點,比如改為5, 從而減少survivor區的占用。
// 修改移動到老年代判斷的標準
-XX:MaxTenuringThreshold=5
-XX:PretenureSizeThreshold=1M? ??直接晉升老年代的對象大小閾值
3. 如果JVM內存超過4G, 就不適合JDK8默認的垃圾收集器(Parallel Scavenge收集器和Parallel Old收集器), 考慮使用ParNew + CMS收集器。
參數配置:
-Xms3072M -Xmx3072M -Xmn2048M
-Xss1M
-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=5
-XX:PretenureSizeThreshold=1M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC // 啟用CMS
-XX:CMSInitiatingOccupancyFraction=92
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=3
垃圾收集底層算法實現
三色標記
三色標記算法是把Gc roots可達性分析遍歷對象過程中遇到的對象, 按照“是否訪問過”這個條件標記成以下三種顏色:
1. 黑色: 表示對象已經被垃圾收集器訪問過, 且這個對象的所有引用都已經掃描過。黑色表示這個對象是安全存活的。
所有引用: A->B->C, A->D, A的所有引用指的是B和D, 并不包括C.
2. 灰色: 表示對象已經被垃圾收集器訪問過, 但這個對象上至少存在一個引用還沒有被掃描過。
3. 白色: 表示對象尚未被垃圾收集器訪問過。 顯然在可達性分析剛剛開始的階段, 所有的對象都是白色的, 若在分析結束的階段,仍然是白色的對象, 即代表不可達。
浮動垃圾(多標了存活對象)
????????并發標記過程中,被標記了的對象有被用戶線程銷毀,這部分本應該回收但是沒有回收到的內存,被稱之為“浮動垃圾”。浮動垃圾并不會影響垃圾回收的正確性,只是需要等到下一輪垃圾回收中才被清除。
????????另外,針對并發標記(還有并發清理)開始后產生的新對象,通常的做法是直接全部當成黑色。
讀寫屏障(漏標了存活對象)
????????漏標會導致被引用的對象被當成垃圾誤刪除,這是嚴重bug,必須解決,有兩種解決方案: 增量更新(Incremental Update) 和原始快照(Snapshot At The Beginning,SATB)。
漏標分析:
![]() | ![]() |
在并發標記的過程中,如果出現b.d = null, a.d = D, 就會變成右邊的引用鏈。
按照三色標記算法,A是黑色,表示A已經掃描完成不會再被掃描,此時D并不是垃圾,但是GC root可達性分析并沒有標記出D,D是會被回收的,這就是漏標,這是不可接受的。
1. 增量更新(Incremental Update)
? ? ? ? 關注增量的引用。當黑色對象插入新的指向白色對象的引用關系時, 就將這個新插入的引用記錄用一個集合記錄下來, 等并發掃描結束之后, 再將這些記錄過的引用關系中的黑色對象為根, 重新掃描一次(重新標記階段)。 這可以簡化理解為, 黑色對象一旦新插入了指向白色對象的引用之后,它就變回灰色對象了。
? ? ? ? 通過寫屏障實現。
2. 原始快照(Snapshot At The Beginning,SATB)
? ? ? ? 關注刪除引用。當灰色對象要刪除指向白色對象的引用關系時, 就將這個要刪除的引用 記錄下來(b.d = null , 那么將d記錄到集合中), 在并發掃描結束之后, 再將這些記錄過的引用關系中的灰色對象為根, 重新掃描一次,這樣就能掃描到白色的對象,將白色對象直接標記為黑色(目的就是讓這種對象在本輪gc清理中能存活下來,待下一輪gc的時候重新掃描,這個對象也有可能是浮動垃圾)
? ? ? ? 通過寫屏障實現。
寫屏障
所謂的寫屏障,其實就是指在賦值操作前后,加入一些處理(可以參考AOP的概念):
void oop_field_store(oop* field, oop new_value) {pre_write_barrier(field); // 寫屏障-寫前操作*field = new_value;post_write_barrier(field, value); // 寫屏障-寫后操作
}
讀屏障
oop oop_field_load(oop* field) {pre_load_barrier(field); // 讀屏障-讀取前操作return *field;
}
對于讀寫屏障,以Java HotSpot VM為例,其并發標記時對漏標的處理方案如下:
CMS:寫屏障 + 增量更新
G1,Shenandoah:寫屏障 + SATB
ZGC:讀屏障
為什么G1用SATB?CMS用增量更新?
簡單理解:SATB相對增量更新效率會高(當然SATB可能造成更多的浮動垃圾),因為不需要在重新標記階段再次深度掃描被刪除引用對象,而CMS對增量引用的根對象會做深度掃描,G1因為很多對象都位于不同的region,CMS就一塊老年代區域,重新深度掃描對象的話G1的代價會比CMS高,所以G1選擇SATB不深度掃描對象,只是簡單標記,等到下一輪GC再深度掃描。