G1 垃圾收集器原理详解
一、G1 垃圾收集器的開發背景:
1、CMS 垃圾收集器的缺陷:
? ? ? ? JVM 團隊設計出 G1 收集器的目的就是取代 CMS 收集器,因為?CMS 收集器在很多場景下存在諸多問題,缺陷暴露無遺,具體如下:
(1)CMS收集器對CPU資源非常敏感。在并發階段,雖然不會導致用戶線程停頓,但是會占用CPU資源而導致引用程序變慢,總吞吐量下降。CMS默認啟動的回收線程數是:(CPU數量+3) / 4
(2)CMS收集器無法處理浮動垃圾,由于CMS并發清理階段用戶線程還在運行,伴隨程序的運行自然會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之后,稱為“浮動垃圾”,CMS 無法在本次收集中處理它們,只好留待下一次GC時將其清理掉。
(3)由于垃圾收集階段會產生“浮動垃圾”,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分內存空間提供并發收集時的程序運作使用。在默認設置下,CMS收集器在老年代使用了68%的空間時就會被激活,也可以通過參數-XX:CMSInitiatingOccupancyFraction的值來提高觸發百分比,以降低內存回收次數提高性能。要是CMS運行期間預留的內存無法滿足程序其他線程需要,就會出現“Concurrent Mode Failure”失敗,這時候虛擬機將啟動后備預案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。所以參數 -XX:CMSInitiatingOccupancyFraction 設置的過高將會很容易導致 “Concurrent Mode Failure” 失敗,性能反而降低。
(4)CMS是基于“標記-清除”算法實現的收集器,會產生大量不連續的內存碎片。當老年代空間碎片太多時,如果無法找到一塊足夠大的連續內存存放對象時,將不得不提前觸發一次Full GC。為了解決這個問題,CMS收集器提供了一個-XX:UseCMSCompactAtFullCollection開關參數,用于在Full ?GC之后增加一個碎片整理過程,還可通過-XX:CMSFullGCBeforeCompaction參數設置執行多少次不壓縮的Full ?GC之后,跟著來一次碎片整理過程。
2、G1 垃圾收集器的特點:
????????G1(Garbage First)收集器是 JDK7 提供的一個新收集器,在 JDK9 中更被指定為官方GC收集器,與CMS收集器相比,最突出的改進是:
- 基于 “標記-整理” 算法,收集后不會產生內存碎片。
- 可以非常精確控制停頓時間,在不犧牲吞吐量前提下,實現低停頓垃圾回收。
在介紹G1的垃圾收集流程之前,我們先簡單了解下G1中的內存模型以及主要的數據結構,這些數據結果對我們了解G1的垃圾回收流程十分重要
二、G1 垃圾收集器的內存模型:
????????G1 收集器不采用傳統的新生代和老年代物理隔離的布局方式,僅在邏輯上劃分新生代和老年代,將整個堆內存劃分為2048個大小相等的獨立內存塊Region,每個Region是邏輯連續的一段內存,具體大小根據堆的實際大小而定,整體被控制在 1M - 32M 之間,且為2的N次冪(1M、2M、4M、8M、16M和32M),并使用不同的Region來表示新生代和老年代,G1不再要求相同類型的 Region 在物理內存上相鄰,而是通過Region的動態分配方式實現邏輯上的連續。
????????G1收集器通過跟蹤Region中的垃圾堆積情況,每次根據設置的垃圾回收時間,回收優先級最高的區域,避免整個新生代或整個老年代的垃圾回收,使得stop the world的時間更短、更可控,同時在有限的時間內可以獲得最高的回收效率。
? ? ? ? 通過區域劃分和優先級區域回收機制,確保G1收集器可以在有限時間獲得最高的垃圾收集效率。
1、分區Region:
? ? ? ? G1 垃圾收集器將堆內存劃分為若干個 Region,每個?Region 分區只能是一種角色,Eden區、S區、老年代O區的其中一個,空白區域代表的是未分配的內存,最后還有個特殊的區域H區(Humongous),專門用于存放巨型對象,如果一個對象的大小超過Region容量的50%以上,G1 就認為這是個巨型對象。在其他垃圾收集器中,這些巨型對象默認會被分配在老年代,但如果它是一個短期存活的巨型對象,放入老年代就會對垃圾收集器造成負面影響,觸發老年代頻繁GC。為了解決這個問題,G1劃分了一個H區專門存放巨型對象,如果一個H區裝不下巨型對象,那么G1會尋找連續的H分區來存儲,如果尋找不到連續的H區的話,就不得不啟動 Full GC 了。
2、Remember Set:
????????在串行和并行收集器中,GC時是通過整堆掃描來確定對象是否處于可達路徑中。然而G1為了避免STW式的整堆掃描,為每個分區各自分配了一個?RSet(Remembered Set),它內部類似于一個反向指針,記錄了其它?Region 對當前?Region 的引用情況,這樣就帶來一個極大的好處:回收某個Region時,不需要執行全堆掃描,只需掃描它的 RSet 就可以找到外部引用,來確定引用本分區內的對象是否存活,進而確定本分區內的對象存活情況,而這些引用就是 initial mark 的根之一。
????????事實上,并非所有的引用都需要記錄在RSet中,如果引用源是本分區的對象,那么就不需要記錄在 RSet 中;同時 G1 每次 GC 時,所有的新生代都會被掃描,因此引用源是年輕代的對象,也不需要在RSet中記錄;所以最終只需要記錄老年代到新生代之間的引用即可。
3、Card Table:
????????如果一個線程修改了Region內部的引用,就必須要去通知RSet,更改其中的記錄。需要注意的是,如果引用的對象很多,賦值器需要對每個引用做處理,賦值器開銷會很大,因此 G1 回收器引入了?Card Table 解決這個問題。
????????一個 Card Table 將一個 Region 在邏輯上劃分為若干個固定大小(介于128到512字節之間)的連續區域,每個區域稱之為卡片 Card,因此 Card 是堆內存中的最小可用粒度,分配的對象會占用物理上連續的若干個卡片,當查找對分區內對象的引用時便可通過卡片 Card 來查找(見RSet),每次對內存的回收,也都是對指定分區的卡片進行處理。每個 Card 都用一個 Byte 來記錄是否修改過,Card Table 就是這些 Byte 的集合,是一個字節數組,由 Card 的數組下標來標識每個分區的空間地址。默認情況下,每個 Card 都未被引用,當一個地址空間被引用時,這個地址空間對應的數組索引的值被標記為”0″,即標記為臟被引用,此外 RSet 也將這個數組下標記錄下來。
? ? ? ? 一個Region可能有多個線程在并發修改,因此也可能會并發修改 RSet。為避免沖突,G1垃圾回收器進一步把 RSet 劃分成了多個 HashTable,每個線程都在各自的 HashTable 里修改。最終,從邏輯上來說,RSet 就是這些 HashTable 的集合。哈希表是實現 RSet 的一種常見方式,它的好處就是能夠去除重復,這意味著,RS的大小將和修改的指針數量相當,而在不去重的情況下,RS的數量和寫操作的數量相當。
HashTable 的 Key 是別的 Region 的起始地址,Value是一個集合,里面的元素是Card Table的Index。
前面三個數據結構的關系如下:
????????圖中RS的虛線表明的是,RSet 并不是一個和 Card Table獨立的、不同的數據結構,而是指RS是一個概念模型。實際上,Card Table 是 RS 的一種實現方式。
G1對內存的使用以分區(Region)為單位,而對對象的分配則以卡片(Card)為單位。
4、RSet 的寫屏障:
????????寫屏障是指,每次 Reference 引用類型在執行寫操作時,都會產生 Write Barrier?寫屏障暫時中斷操作并額外執行一些動作。
????????對寫屏障來說,過濾掉不必要的寫操作是十分有必要的,因為寫柵欄的指令開銷是十分昂貴的,這樣既能加快賦值器的速度,也能減輕回收器的負擔。G1 收集器的寫屏障是跟 RSet 相輔相成的,產生寫屏障時會檢查要寫入的引用指向的對象是否和該 Reference 類型數據在不同的 Region,如果不同,才通過 CardTable 把相關引用信息記錄到引用指向對象的所在 Region 對應的 RSet?中,通過過濾就能使 RSet?大大減少。
(1)寫前柵欄:即將執行一段賦值語句時,等式左側對象將修改引用到另一個對象,那么等式左側對象原先引用的對象所在分區將因此喪失一個引用,那么JVM就需要在賦值語句生效之前,記錄喪失引用的對象。但JVM并不會立即維護RSet,而是通過批量處理,在將來RSet更新
(2)寫后柵欄:當執行一段賦值語句后,等式右側對象獲取了左側對象的引用,那么等式右側對象所在分區的RSet也應該得到更新。同樣為了降低開銷,寫后柵欄發生后,RSet也不會立即更新,同樣只是記錄此次更新日志,在將來批量處理
????????G1垃圾回收器進行垃圾回收時,在GC根節點枚舉范圍加入RSet,就可以保證不進行全局掃描,也不會有遺漏。另外JVM使用的其余的分代的垃圾回收器也都有寫屏障,舉例來說,每次將一個老年代對象的引用修改為指向年輕代對象,都會被寫屏障捕獲并記錄下來,因此在年輕代回收的時候,就可以避免掃描整個老年代來查找根。
????????G1的垃圾回收器的寫屏障使用一種兩級的log buffer結構:
- global set of filled buffer:所有線程共享的一個全局的,存放填滿了的log buffer的集合
- thread log buffer:每個線程自己的log buffer。所有的線程都會把寫屏障的記錄先放進去自己的log buffer中,裝滿了之后,就會把log buffer放到 global set of filled buffer中,而后再申請一個log buffer;
5、Collect Set:
????????Collect Set(CSet)是指,在 Evacuation 階段,由G1垃圾回收器選擇的待回收的Region集合,在任意一次收集器中,CSet 所有分區都會被釋放,內部存活的對象都會被轉移到分配的空閑分區中。G1 的軟實時性就是通過CSet的選擇來實現的,對應于算法的兩種模式 fully-young generational mode 和 partially-young mode,CSet的選擇可以分成兩種:
????????候選老年代分區的CSet準入條件,可以通過活躍度閾值 -XX:G1MixedGCLiveThresholdPercent(默認85%) 進行設置,從而攔截那些回收開銷巨大的對象;同時,每次混合收集可以包含候選老年代分區,可根據CSet對堆的總大小占比 -XX:G1OldCSetRegionThresholdPercent(默認10%) 設置數量上限。
????????由上述可知,G1的收集都是根據CSet進行操作的,年輕代收集與混合收集沒有明顯的不同,最大的區別在于兩種收集的觸發條件。
三、G1的的垃圾收集過程:
????????G1提供了兩種GC模式,Young GC 和 Mixed GC,兩種都是Stop The World(STW)的,不過講垃圾回收之前,我們先介紹下 G1 的對象分配策略。
1、對象分配策略:
????????每一個分配的 Region 都可以分成兩個部分,已分配的和未被分配的,它們之間的界限被稱為top。總體上來說,把一個對象分配到Region內,只需要簡單增加top的值。過程如下:
(1)線程本地分配緩沖區 Thread Local allocation buffer (TLab):
????????如果對象在一個共享的空間中分配,那么我們就需要采用同步機制來解決并發沖突問題,而為了減少并發沖突損耗的同步時間,G1 為每個應用線程和GC線程分配了一個本地分配緩沖區TLAB,分配對象內存時,就在這個 buffer 內分配,線程之間不再需要進行任何的同步,提高GC效率。但是當線程耗盡了自己的Buffer之后,需要申請新的Buffer。這個時候依然會帶來并發的問題。G1回收器采用的是CAS(Compate And Swap)操作。
????????顯然的,采用TLAB的技術,就會帶來碎片。舉例來說,當一個線程在自己的Buffer里面分配的時候,雖然Buffer里面還有剩余的空間,但是卻因為分配的對象過大以至于這些空閑空間無法容納,此時線程只能去申請新的Buffer,而原來的Buffer中的空閑空間就被浪費了。Buffer的大小和線程數量都會影響這些碎片的多寡。
? ? ? ? 每次垃圾收集時,每個GC線程同樣可以獨占一個本地緩沖區(GCLAB)用來轉移對象,將存活對象復制到Suvivor空間或老年代空間;
????????對于從Eden/Survivor空間晉升(Promotion)到Survivor/老年代空間的對象,同樣有GC獨占的本地緩沖區進行操作,該部分稱為晉升本地緩沖區(PLAB)。
(2)Eden區中分配:
????????對TLAB空間中無法分配的對象,JVM會嘗試在Eden空間中進行分配。如果Eden空間無法容納該對象,就只能在老年代中進行分配空間。
(3)Humongous區分配:
????????巨型對象會獨占一個、或多個連續分區,其中第一個分區被標記為開始巨型(StartsHumongous),相鄰連續分區被標記為連續巨型(ContinuesHumongous)。由于無法享受 TLab 帶來的優化,并且確定一片連續的內存空間需要掃描整堆,因此確定巨型對象開始位置的成本非常高,如果可以,應用程序應避免生成巨型對象。
G1內部做了一個優化,一旦發現沒有引用指向巨型對象,則可直接在年輕代收集周期中被回收。
2、G1 Young GC:
????????當Eden區已滿,JVM分配對象到Eden區失敗時,便會觸發一次STW式的年輕代收集young GC,將 Eden 區存活的對象將被拷貝到 to?survivor 區;from survivor 區存活的對象則根據存活次數閾值分別晉升到 PLAB、to survivor 區和老年代中;如果 survivor 空間不夠,Eden區的部分數據會直接晉升到年老代空間。最終Eden空間的數據為空,GC停止工作,應用線程繼續執行。
? ? ? ? young GC 還負責維護對象的年齡(存活次數),輔助判斷老化(tenuring)對象晉升時的去向。young GC 首先將晉升對象尺寸總和、年齡信息維護到年齡表中,再根據年齡表、Survivor尺寸、Survivor填充容量 -XX:TargetSurvivorRatio(默認50%)、最大任期閾值 -XX:MaxTenuringThreshold(默認15),計算出一個恰當的任期閾值,凡是超過任期閾值的對象都會被晉升到老年代。
????????這時,我們需要考慮一個問題,如果僅僅 GC 新生代對象,我們如何找到所有的根對象呢? 老年代的所有對象都是根么?那這樣掃描下來會耗費大量的時間。于是就需要使用到我們上文介紹到的?RSet 了,RSet 中記錄了其他 region 對當前 region 的引用,因此,在進行Young GC 時,掃描根時,僅僅需要掃描這一塊區域,而不需要掃描整個老年代。
2.1、young GC的詳細回收過程:
(1)第一階段,根掃描:
根是指static變量指向的對象、正在執行的方法調用鏈上的局部變量等。根引用連同 RSet 記錄的外部引用作為掃描存活對象的入口。
(2)第二階段,更新RSet:
處理 dirty card 隊列中的 card,更新 RSet,此階段完成后,RSet 可以準確的反映老年代對所在的region 分區中對象的引用
(3)第三階段:處理RSet:
識別被老年代對象指向的 Eden 中的對象,這些被指向的Eden中的對象被認為是存活的對象
(4)第四階段:對象拷貝:
將 Eden 區存活的對象將被拷貝到 to?survivor 區;from survivor 區存活的對象則根據存活次數閾值分別晉升到 PLAB、to survivor 區和老年代中;如果 survivor 空間不夠,Eden區的部分數據會直接晉升到年老代空間。
(5)第五階段:處理引用:
處理軟引用、弱引用、虛引用,最終Eden空間的數據為空,GC停止工作,而目標內存中的對象都是連續存儲的、沒有碎片,所以復制過程可以達到內存整理的效果,減少碎片。
3、G1 Mixed GC:
????????年輕代不斷進行垃圾回收活動后,為了避免老年代的空間被耗盡。當老年代占用空間超過整堆比 IHOP 閾值 -XX:InitiatingHeapOccupancyPercent(默認45%)時,G1就會啟動一次混合垃圾回收Mixed GC,Mixed GC不僅進行正常的新生代垃圾收集,同時也回收部分后臺掃描線程標記的老年代分區。Mixed GC步驟主要分為兩步:
(1)全局并發標記(global concurrent marking)
(2)拷貝存活對象(evacuation)
這里需要特別注意的是 Mixed GC 并不是 Full GC,只有當?Mixed GC 來不及回收old region,也就說在需要分配老年代的對象時,但發現沒有足夠的空間,這個時候就會觸發一次 Full GC
3.1、全局并發標記(global concurrent marking)
????????在進行混合回收前,會先進行 global concurrent marking,在 G1 GC 中,它并不是一次GC過程的必須環節,主要是為 Mixed GC 提供標記服務的。global concurrent marking的執行過程分為五個步驟:
(1)初始標記(initial mark,STW):
會標記出所有?GC Roots 節點以及直接可達的對象,這一階段需stop the world,但是耗時很短。
????????初始標記過程與 young GC 息息相關。事實上,當達到 IHOP 閾值時,G1并不會立即發起并發標記周期,而是等待下一次年輕代收集,利用年輕代收集的STW時間段,完成初始標記,這種方式稱為借道。
(2)根區域掃描(root region scan):
掃描初始標記的存活區中(即 survivor 區)可直達的老年代區域對象,并標記根對象。該階段與應用程序并發運行,并且只有完成該階段后,才能開始下一次 STW 的 young GC。
因為 RSet 是不記錄從 young region 出發的引用,那么就可能出現一種情況,一個老年代的存活對象,只被年輕代的對象引用。在一次young GC中,這些存活的年輕代的對象會被復制到 Survivor Region,因此需要掃描這些 Survivor region 來查找這些指向老年代的對象的引用,作為并發標記階段掃描老年代的根的一部分。
(3)并發標記(Concurrent Marking):
從?GC Roots 對堆中的對象進行可達性分析,找出存活的對象,此過程可能被 young GC 中斷,并發標記階段產生的新的引用(或引用的更新)會被 SATB 的 write barrier 記錄下來,同時,并發標記線程還會定期檢查和處理STAB全局緩沖區列表的記錄,更新對象引用信息。在此階段中,如果發現區域中的所有對象都是垃圾,那這個區域會立即被回收。同時,并發標記過程中,會計算每個區域中對象的存活比例。
在并發標記階段,我們就不得不了解一下三色標記算法,該算法我們放在下文介紹
(4)重新標記(Remark,STW):
重新標記階段是為了修正在并發標記期間,因應用程序繼續運作而導致標記產生變動的那一部分標記記錄,就是去處理剩下的?SATB日志緩沖區和所有更新,找出所有未被訪問的存活對象。
????????CMS收集器中,重新標記使用的增量更新,而 G1 使用的是比 CMS 更快的初始快照算法?SATB 算法:snapshot-at-the-beginning。
????????SATB 在標記開始時會創建一個存活對象的快照圖,從而確保并發標記階段所有的垃圾對象都能通過快照被鑒別出來。當賦值語句發生時,應用將會改變了它的對象圖,那么JVM需要記錄被覆蓋的對象,因此寫前柵欄會在引用變更前,將值記錄在SATB日志或緩沖區中(每個線程都會獨占一個SATB緩沖區,初始有256條記錄空間)。當空間用盡時,線程會分配新的SATB緩沖區繼續使用,而原有的緩沖去則加入全局列表中。最終在并發標記階段,并發標記線程在標記的同時,還會定期檢查和處理全局緩沖區列表的記錄,然后根據標記位圖分片的標記位,掃描引用字段來更新RSet,修正 SATB? 的誤差。
????????SATB的 log buffer 如 RSet 的寫屏障使用的 log buffer 一樣,都是兩級結構,作用機制也是一樣的。
(5)清除(Cleanup,STW):
該階段主要是排序各個?Region 的回收價值和成本,并根據用戶所期望的GC停頓時間來制定回收計劃。(這個階段并不會實際去做垃圾的回收,也不會執行存活對象的拷貝)
清除階段執行的詳細操作有一下幾點:
① RSet梳理:啟發式算法會根據活躍度和RSet尺寸對分區定義不同等級,同時RSet數理也有助于發現無用的引用。
② 整理堆分區:為混合收集識別回收收益高(基于釋放空間和暫停目標)的老年代分區集合;
③ 識別所有空閑分區:即發現無存活對象的分區,該分區可在清除階段直接回收,無需等待下次收集周期。
如果不考慮維護Remembered Set的操作,可以分為上圖4個步驟(與CMS較為相似),其中初始標記、并發標記、重新標記跟CMS收集器相同,只有第四階段的篩選回收有些區別。
3.2、拷貝存活對象(evacuation):
當G1發起全局并發標記之后,并不會馬上開始混合收集,G1會先等待下一次年輕代收集,然后在該 young gc 收集階段中,確定下次混合收集的CSet
? ? ? ? 全局標記完成后,G1 就知道哪些 old region 的可回收垃圾最多了,只需等待合適的時機就可以開始混合回收了,而混合回收除了回收這個young region,還會回收部分 old region(不需要回收全部 old region)。根據停頓目標,G1 可能沒法一次回收掉所有的old region 候選分區,只能選擇優先級高的若干個 region 進行回收,所以G1可能會產生連續多次的混合收集與應用線程交替執行,而這些被選中的 region 就是 CSet 了,而單次的混合回收的算法與上文的 Young GC 算法完全一樣,只不過回收集CSet 中多了老年代的內存分段;而第二個步驟就是將這些 region 中存活的對象復制到空間 region 中去,同時把這些已經被回收的 region 放到空閑 region 列表中。
????????G1會計算每次加入到CSet中的分區數量、混合收集進行次數,并且在上次的年輕代收集、以及接下來的混合收集中,G1會確定下次加入CSet的分區集(Choose CSet),并且確定是否結束混合收集周期。
(1)并發標記結束以后,老年代中100%為垃圾的 region 就直接被回收了,僅部分為垃圾的region會被分成8次回收(可以通過 -XX:G1MixedGCCountTarget 設置,默認閾值8),所以 Mixed GC 的回收集(CSet)包括八分之一的老年代內存分段、Eden 區內存分段、Survivor 區內存分段。
(2)由于老年代的內存分段默認分8次回收,G1會優先回收垃圾多的內存分段。垃圾占內存分段比例越高的,越會被先回收。并且由一個閾值決定內存分段是否被回收?-XX:G1MixedGCLiveThresholdPercent,默認為 65%,意思是垃圾占內存分段比例要達到 65% 才會被回收。如果垃圾占比太低,意味著存活的對象占比高,在復制的時候會花費更多的時間。
(3)混合回收并不一定要進行8次,有一個閾值?-XX:G1HeapWastePercent,默認值 10%,意思是允許整個堆內存有 10% 的空間浪費,意味著如果發現可以回收的垃圾占堆內存的比例低于10%,則不再進行混合回收,因為 GC 會花費很多的時間,但是回收到的內存卻很少
G1 垃圾回收流程小結:Young CG 和 Mixed GC,是G1回收空間的主要活動。當應用開始運行時,堆內存可用空間還比較大,只會在年輕代滿時,觸發年輕代收集;隨著老年代內存增長,當到達 IHOP 閾值 -XX:InitiatingHeapOccupancyPercent(老年代占整堆比,默認45%) 時,G1開始著手準備收集老年代空間。首先經歷并發標記周期,識別出高收益的老年代分區。但隨后G1并不會馬上開啟一次混合收集,而是讓應用線程先運行一段時間,等待觸發一次年輕代收集,在這次STW中,G1將開始整理混合收集周期。接著再次讓應用線程運行,當接下來的幾次年輕代收集時,將會有老年代分區加入到CSet中,即觸發混合收集,這些連續多次的混合收集稱為混合收集。
4、Full GC:
????????當 G1 無法在堆空間中申請新的分區時,G1便會觸發擔保機制,執行一次STW式的、單線程的 Full GC,Full GC會對整堆做標記清除和壓縮,最后將只包含純粹的存活對象。參數-XX:G1ReservePercent(默認10%)可以保留空間,來應對晉升模式下的異常情況,最大占用整堆50%,更大也無意義。
????????G1在以下場景中會觸發 Full GC,同時會在日志中記錄to-space-exhausted以及Evacuation Failure:
- (1)從年輕代分區拷貝存活對象時,無法找到可用的空閑分區
- (2)從老年代分區轉移存活對象時,無法找到可用的空閑分區
- (3)分配巨型對象時在老年代無法找到足夠的連續分區
????????由于G1的應用場合往往堆內存都比較大,所以Full GC的收集代價非常昂貴,應該避免Full GC的發生。
5、三色標記算法:
三色標記算法是并發收集階段的重要算法,它是描述追蹤式回收器的一種有用的方法,利用它可以推演回收器的正確性。 首先,我們將對象分成三種類型的。
- 黑色:根對象,或者該對象與它的子對象都被掃描了
- 灰色:對象本身被掃描,但還沒掃描完該對象中的子對象
- 白色:未被掃描對象,掃描完成所有對象之后,最終為白色的為不可達對象,即垃圾對象
下面我們就以一組演變圖,加深下對三色標記算法的理解,當GC開始掃描對象時,按照如下圖步驟進行對象的掃描:
5.1、三色標記算法的正常流程:
(1)根對象被置為黑色,子對象被置為灰色:
?(2)繼續由灰色遍歷,將已掃描了子對象的對象置為黑色。
(3)遍歷了所有可達的對象后,所有可達的對象都變成了黑色。不可達的對象即為白色,需要被清理。
5.2、三色標記算法的異常情況:
????????如果在標記過程中,應用程序也在運行,那么對象的指針就有可能改變。這樣的話,我們就會遇到一個問題:對象丟失問題。我們看下面一種情況,當垃圾收集器掃描到下面情況時:
?這時候應用程序執行了以下操作:
A.c=C
B.c=null
這樣,對象的狀態圖變成如下情形:
?這時候垃圾收集器再標記掃描的時候就會下圖成這樣:
很顯然,此時C是白色,被認為是垃圾需要清理掉,顯然這是不合理的。那么我們如何保證應用程序在運行的時候,GC標記的對象不丟失呢?有如下兩種可行的方式:
- 在插入的時候記錄對象
- 在刪除的時候記錄對象
剛好這對應 CMS?和 G1 的兩種不同實現方式:
(1)CMS采用的是增量更新(Incremental update):只要在寫屏障里發現要有一個白對象的引用被賦值到一個黑對象的字段里,那就把這個白對象變成灰色的,即插入的時候記錄下來。
(2)G1 使用的是 STAB(snapshot-at-the-beginning)的方式:刪除的時候記錄所有的對象,它有三個步驟:
- ① 在開始標記的時候生成一個存活對象的快照圖
- ② 在并發標記時,所有被改變的對象入隊(在write barrier里把所有舊的引用所指向的對象都變成非白的)
- ③ 可能存在游離的垃圾,將在下次被收集
四、G1 收集器的缺點:
(1)如果停頓時間過短的話,可能導致每次選出的回收集只占堆內存很小一部分,收集器收集的速度逐漸跟不上分配器的分配速度,進而導致垃圾慢慢堆積,最終造成堆空間占滿,引發Full GC 反而降低性能。
(2)G1 無論是在垃圾收集產生的內存占用還是程序運行時的額外執行負載都要比CMS要高
(3)CMS在小內存應用上大概率由于 G1。所以小內存的情況下使用CMS收集器,大內存的情況下可以使用G1收集器(G1收集器6GB以上)
參考文章:
https://www.cnblogs.com/lsgxeva/p/10231201.html
https://blog.csdn.net/weixin_43899069/article/details/117996701
https://blog.csdn.net/coderlius/article/details/79272773
https://www.jianshu.com/p/aef0f4765098
總結
以上是生活随笔為你收集整理的G1 垃圾收集器原理详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java集合篇:HashMap 与 Co
- 下一篇: AES前后端加密解密