一、對象是否已死
1. 引用計數(shù)
在對象中添加一個引用計數(shù)器,每當有一個地方引用它時就加一,失效時減一。
優(yōu)點:實現(xiàn)簡單;
缺點:無法解決循環(huán)引用;
2. 可達性分析
通過GCRoot作為起點集向下遍歷調用鏈,不在調用鏈上的對象就是不可能再被使用的對象。
GCRoots包括以下幾種:
1. 棧中引用的對象,即棧中局部變量表LocalVariableTable;
2. 方法區(qū)中static和final屬性引用的對象,例如常量池;
3. 本地方法棧中Native方法引用的對象;
4. JVM內部需要使用的常駐對象,例如基本數(shù)據(jù)類型對應的Class對象、系統(tǒng)類加載器、異常對象、JMXBean等等;
5. synchronized持有的對象;
引用類型:
1. 強引用程序代碼中普遍存在的引用賦值;
2. 軟引用正常情況下不到回收,內存溢出前回收;
3. 弱引用垃圾收集時發(fā)現(xiàn)它就會回收;
4. 虛引用弱引用的一種特殊情況,只是為了被回收時進行通知回調設計的,例如堆外內存的回收
方法區(qū)回收:
1. 常量沒有任何對象引用常量池中該常量時可能會被回收;
2. Class回收需要同時滿足三個條件:堆中不存在該Class的實例 && 該類加載器已經(jīng)被回收 && Class沒有被任何地方引用(排除正在反射的情況);可以看到Class被回收的條件是很苛刻的,現(xiàn)在第三方框架大量使用反射、動態(tài)代理、CGLIB的生成Class,給Metaspace帶來壓力;
二、垃圾收集算法
1. 標記清除
1960年提出,標記出所有需要回收的對象,然后統(tǒng)一回收掉;
優(yōu)點:簡單;不需要移動存活對象;
缺點:執(zhí)行效率跟堆中對象數(shù)量成反比,對象非常多時效率低;清除后產(chǎn)生大量內存碎片;
2. 標記復制
1969年提出,為了解決標記清除算法大量對象效率低的問題;新生代98%熬不過第一輪,不需要按照1:1劃分新生代內存空間,1989年提出新生代分為一塊較大的Eden區(qū)和兩塊較小的Survivor區(qū),每次把Eden和Survivor中仍存活的對象一次性復制到另外一塊Survivor上;
優(yōu)點:沒有碎片;分配內存時只需要移動堆頂指針按順序分配,簡單高效;
缺點:空間浪費;需要移動存活的對象;
3. 標記整理
1974年提出,為了解決標記復制浪費空間的問題;先標記存活對象然后讓所有存活的對象移動到一端,然后直接清理掉另一端內存;
標記清除和標記整理區(qū)別是前者不需要移動存活對象屬于停頓時間優(yōu)先,分配時需要找到可分配區(qū)域,相對麻煩;回收時直接清理,相對簡單;后者正好相反屬于吞吐量優(yōu)先;
優(yōu)點:沒有空間浪費;沒有內存碎片;
缺點:需要移動存活對象;
三、垃圾收集器
1. Serial / Serial Old收集器
單線程收集器,新生代Serial采用復制算法,老年代Serial Old采用整理算法;
優(yōu)點:簡單,額外內存消耗最??;
缺點:STW;
2. Parallel Scavenge / Parallel Old收集器
吞吐量優(yōu)先的多線程收集器,新生代Parallel Scavenge采用復制算法,老年代Parallel Old采用整理算法;新增兩個參數(shù)用于精確控制吞吐量,最大垃圾收集停頓時間:-XX:MaxGCPauseMillis,吞吐量大?。?XXGCTimeRatio;
3. ParNew收集器
新生代收集器采用復制算法,Serial的多線程版本,可以與CMS配合;
4. CMS收集器(重點)
老年代收集器采用清除算法,過程分為四個步驟:
初始標記:單線程標記GC Roots能直接關聯(lián)到的對象,需要STW但是速度很快;
并發(fā)標記:多線程從初始標記的對象開始遍歷整個對象圖,過程較長,但是可以跟用戶線程并行執(zhí)行;
重新標記:多線程對并發(fā)標記期間產(chǎn)生變動的對象進行重新標記,過程比初始標記長,但是遠比并發(fā)標記短;
并發(fā)清除:清理被標記為死亡的對象,不需要移動存活對象,因此可以跟用戶現(xiàn)場并行執(zhí)行;
4.1 如何進行標記?
三色標記法把遍歷對象圖過程中遇到的對象,按照“是否被垃圾收集訪問過”這個條件標記成以下三種顏色:
白色:尚未被垃圾收集訪問過,開始時都是白色,最后剩下的白色就是不可達的可回收對象;
黑色:已經(jīng)被垃圾收集訪問過,并且這個對象所有的引用都已經(jīng)掃描過,表示可達的存活對象;
灰色:對象本身已經(jīng)被垃圾收集訪問過,但是對象上還有未被訪問過的引用;
4.2 三色標記產(chǎn)生的問題
1.浮動垃圾

如圖已經(jīng)掃描過標記為黑色的對象引用斷開,此時這個被斷開的黑色對象這一輪垃圾回收時就不會再被掃描到并且一直是黑色,這一輪不會被回收,就產(chǎn)生了浮動垃圾。浮動垃圾理論上下一輪回收就會被回收掉,這個問題不大。
2.對象消失

如圖灰色對象到白色對象的引用突然效時,并且已經(jīng)掃描過的黑色對象又指向了該白色對象,由于黑色對象本輪垃圾回收已經(jīng)被掃描過,導致該白色對象不會變黑,到并發(fā)清理階段會被當作垃圾清除掉。這種存活的對象被回收會產(chǎn)生嚴重的后果。
4.3 對象消失解決方案
1.增量更新
當黑色對象插入新的指向白色對象的引用關系時,就將這個引用記錄下來,等并發(fā)掃描之后再對記錄的這些引用重新掃描。(相當于黑色引用白色時就變?yōu)榛疑?CMS采用這種方案。
2.原始快照
當灰色對象要刪除指向白色對象的引用關系時,就將這個要刪除的引用記錄下來,在并發(fā)標記結束后再將這些記錄過的引用關系中的灰色對象為根,重新掃描。(相當于無論引用關系刪除與否,都會按照最初開始掃描那一刻的對象圖快照來進行掃描)G1、Shenandoah采用這種方案。
解決方案有了,那如何實現(xiàn)呢,即引用記錄在哪兒?又如何記錄引用?
3.記錄在哪兒?-記憶集與卡表
記憶集是用來記錄從非收集區(qū)指向收集區(qū)的指針集合的抽象數(shù)據(jù)結構,這種記錄無論空間占用還是維護成本都相當高昂,為了降低成本通常有三種記錄粒度:字節(jié)精度(32/64位)、對象精度(精確到每一個對象)、卡精度(精確到一塊內存區(qū)域);
卡表就是用來實現(xiàn)卡精度記憶集的數(shù)據(jù)結構,卡表含有多個卡頁,一個卡頁又包含多個對象,只要卡頁中有一個跨代應用就重新掃描整個卡頁,這樣在跨代引用概率很低時可以節(jié)約維護成本。
卡表的思想可以類比現(xiàn)在新冠檢測采取的策略:在已知感染者概率很低的前提下,讓一批人公用一份檢測試劑,如果該試劑結果呈陰性說明這一批人都沒有被感染;如果呈陽性,那需要讓這一批人全部重新單獨進行檢測;
4.如何記錄?-寫屏障
HotSpot虛擬機采用寫屏障技術維護卡表狀態(tài),寫屏障可以看作在JVM層面對引用類型字段賦值這個操走的AOP切面,在賦值前的操作叫寫前屏障,之后的叫寫后屏障。在寫前屏障中來對卡表進行維護。
4.4 CMS優(yōu)缺點
優(yōu)點劃分步驟的思想具有劃時代的意義;高并發(fā)低停頓;
缺點
1.跟用戶線程并行的階段占用處理器資源導致應用程序變慢;
2.浮動垃圾的問題并未解決,不能等到老年代快滿了再收集,需要預留組構內存空間給用戶線程使用(預留空間不足時會STW,臨時啟用Serial Old收集器);
3.由于采用清除算法,那么就會產(chǎn)生內存碎片,在fullgc時會對碎片進行整理,整理時會移動存活對象需要STW,這樣碎片問題解決了但停頓時間又會變長;
5. G1收集器(重點)
G1采用基于Region的堆內存布局,堆內存不再是固定大小的新生代/老年代,而是由若干個Region組成,每個Regin都可以扮演新生代、老年代、大對象區(qū)(存放超過一定大小的大對象)的角色。G1收集器會評估每個Region的回收價值維護一個優(yōu)先級列表中,每次收集時以Region為單位回收多個Region。

G1回收步驟也分4步:
初始標記:標記GC Roots能直接關聯(lián)到的對象,并修改TAMS指針值(為了跟用戶線程并行執(zhí)行,每個Region通過TAMS指針劃分出來用于并發(fā)回收過程中新對象分配),該階段停頓時間極短;并發(fā)標記:從GC Roots關聯(lián)的對象開始掃描整個對象圖,該階段耗時較長可以并發(fā)執(zhí)行;掃描完后還要重新處理STAB(原始快照)記錄下的并發(fā)時有引用變動的對象;最終標記:短暫的STW,用于處理并發(fā)階段結束后仍遺留下來的最后少量的SATB記錄;篩選回收:負責更新Region的統(tǒng)計數(shù)據(jù),對各個Region價值進行排序,決定回收哪些Region;回收時把待清理Region中剩余對象全量復制到空Region中,然后清空Region;該階段涉及存活對象的移動,需要STW,但是可以多個線程并行操作;
5.1 G1優(yōu)缺點
優(yōu)點:
1.可以通過指定期望停頓時間,在不同場景下在吞吐量和延遲時間之間做平衡,因為G1追求內存分配速率,因此不同的停頓時間只要分配速度大于回收速度就可以;
2.分Region回收,可以按照收益動態(tài)確定回收哪個;
3.沒有內存碎片;
缺點:
1.需要通過卡表存儲各個Region之間的引用,額外內存占用10%-20%;
2.需要記錄Region之間的雙向應用,處理Region之間的引用更加復雜;
3.除了需要通過寫后屏障更新卡表外,還要使用寫前屏障跟蹤并發(fā)時的指針變化情況;相比增量更新算法,原始快照能減少并發(fā)標記和重新標記階段的消耗,避免CMS那樣在最終標記階段停頓時間過長的缺點,但是在用戶程序運行過程中確實會產(chǎn)生由跟蹤引用變化帶來的額外負擔。
----------over---------