G1 垃圾回收器
一、垃圾收集器簡介
1、發展歷程
-
第一階段,Serial(串行)收集器
在jdk1.3.1之前,java虛擬機僅僅能使用Serial收集器。 Serial收集器是一個單線程的收集器,但它的“單線程”的意義并不僅僅是說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。 -
第二階段,Parallel(并行)收集器
Parallel收集器也稱吞吐量收集器,相比Serial收集器,Parallel最主要的優勢在于使用多線程去完成垃圾清理工作,這樣可以充分利用多核的特性,大幅降低gc時間。 -
第三階段,CMS(并發)收集器
CMS收集器在Minor GC時會暫停所有的應用線程,并以多線程的方式進行垃圾回收。在Full GC時不再暫停應用線程,而是使用若干個后臺線程定期的對老年代空間進行掃描,及時回收其中不再使用的對象。 -
第四階段,G1(并發)收集器
G1收集器(或者垃圾優先收集器)的設計初衷是為了盡量縮短處理超大堆(大于4GB)時產生的停頓。相對于CMS的優勢而言是內存碎片的產生率大大降低。
2、種類
-
新生代
- Serial (第一代)
- PraNew (第二代)
- Parallel Scavenge (第三代)
- G1收集器(第四代)
-
老年代
- Serial Old (第一代)
- Parallel Old (第二代)
- CMS (第三代)
- G1
二、G1 介紹
1、概述
- G1最大的特點是引入分區的思路,弱化了分代的概念
- 合理利用垃圾收集各個周期的資源,解決了其他收集器甚至CMS的眾多缺陷
- 算法:基于標記-整理算法,不會產生空間碎片,分配大對象時不會因得不到連續空間而提前觸發 FULL GC
- 停頓時間可控: G1可以通過設置預期停頓時間 Pause Time 來控制垃圾收集時間避免應用雪崩現象
- 并行與并發:G1 能充分利用 多核 CPU 的硬件優勢來縮短 stop the world 的停頓時間
- CMS 的堆分為 PermGen、YoungGen、OldGen;而YoungGen又分了兩個survivo區域
G1 的堆被分為區域(region),每個區域雖然保留了新老代概念,但收集器是以整個區域為單位收集 - G1在回收內存后會馬上同時做合并空閑內存的工作、而 CMS 默認是在STW(stop the world)時做
- G1會在 Young GC 中使用、而 CMS 只能在 O 區使用
- G1垃圾收集算法主要應用在多CPU大內存的服務中,在滿足高吞吐量的同時,盡可能的滿足垃圾回收時的暫停時間。
- 就目前而言、CMS還是默認首選的GC策略、可能在以下場景下G1更適合:
- 服務端多核CPU、JVM內存占用較大的應用(至少大于4G)
- 應用在運行過程中會產生大量內存碎片、需要經常壓縮空間
- 想要更可控、可預期的GC停頓周期,防止高并發下應用雪崩現象
2、G1的堆內存算法
G1之前的JVM內存模型:
G1收集器的內存模型:
-
G1 收集器將整個Java 堆劃分成約 2048 個大小相同的獨立 Region 塊,每個Region塊大小根據堆空間的實際大小而定
可以通過 -XX :G1HeapRegionSize 設定,值在 1MB~32MB 之間且為 2 的次冪
-
region 可能屬于 Eden、Survivor、0ld、Humongous 區域,但一個region只可能屬于一個角色
-
Humongous 作用:用來專門存放大對象,一般使用連續 region 區存儲,G1的大多數行為都把H區
作為老年代的一部分來看待
3、G1 的特點、缺點
3.1 特點
①. 并行和并發
- 并行性:G1 在回收期間,可以有多個GC線程同時工作,有效利用多核計算能力
- 并發性:G1擁有與應用程序交替執行的能力,部分工作可以和應用程序同時執行
②. 分代收集
- 從分代上看,G1 仍屬于分代型垃圾回收器,會區分年輕代和老年代,年輕代依然有 Eden 區和 Survivor 區
但從堆的結構上看,不要求整個 Eden 區、年輕代或老年代都連續,也不再堅持固定大小和固定數量 - 將堆空間分為若干個區域(Region),這些區域中包含了邏輯上的年輕代和老年代
- 和之前的各類回收器不同,其同時兼顧年輕代和老年代
③. 空間整合
- 內存回收以 region 為基本單位,Region之間是復制算法,但整體是標記-壓縮算法,可以避免內存碎片,有利于程序長時間運行,分配大對象時不會因為無法找到連續內存空間而提前觸發下一次GC
④. 可預測的停頓時間模型(即:軟實時soft real一time)
可預測的停頓時間模型:能讓使用者明確指定在一個長度為 M 毫秒的時間片段內,消耗在垃圾收集上的時間不得超過 N 毫秒,通過參數 -XX:MaxGCPauseMillis 設置
- 由于分區的原因,G1可以只選取部分區域進行內存回收,縮小回收范圍,避免全局停頓情況的發生
- G1 跟蹤各個 Region 中垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在后臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的 Region,保證了G1收集器在有限的時間內可以獲取盡可能高的收集效率。
- 相比于 CMS GC,G1未必能做到CMS在最好情況下的延時停頓,但最差情況要好很多
3.2 缺點
- 相較于CMS,G1 還不具備全方位、壓倒性優勢,如:G1 無論是為了垃圾收集產生的內存占用,還是程序運行時的額外執行負載都要比 CMS 高
- 小內存應用上,CMS 的表現大概率會優于 G1,而 G1 在大內存應用上則發揮其優勢,平衡點在6-8GB之間
4、G1 獨有概念
4.1 RSet 與 card
RSet 與 card 專門用來處理 Old 區到 Young 區的引用
Young 區到 Old 區的引用不需要單獨處理,因為 Young 區中的對象本身變化比較大,沒必要浪費空間去記錄下來
- RSet:用來記錄外部指向本 Region 的所有引用,每個 Region 維護一個 RSet
- Card: JVM 將內存劃分成了固定大小的 Card,類比物理內存上 page 的概念
- 每個 Region 分成多個 Card,其中綠色部分的 Card 表示該 Card 中有對象引用了其他 Card 中的對象,這種引用關系用藍色實線表示。
- RSet 其實是一個HashTable,Key 是 Region 的起始地址,Value 是Card Table(字節數組),字節數組下標表示 Card 的空間地址,當該地址空間被引用時會被標記為 dirty_card
RSet 的更新:
G1 采用 post-write barrier 和 concurrent refinement threads 更新 RSet,減少每次給引用類型的字段賦值都要更新 RSet帶來的開銷
java 層面給 old 對象的 p 字段賦值 young 對象之后,jvm 底層會執行 oop_store 方法
在賦值動作的前后,JVM插入一個 pre-write barrier 和 post-write barrier
post-write barrier 的最終動作如下:
- 找到該字段所在的位置(Card),并設置為 dirty_card
- 若當前是應用線程,每個Java線程有一個 dirty card queue,把該 card 插入隊列
- 除了每個線程自帶的 dirty card queue,還有一個全局共享的 queue
RSet 更新操作交由多個 ConcurrentG1RefineThread 并發完成,每當全局隊列集合超過一定閾值后,ConcurrentG1RefineThread 會取出若干個隊列,遍歷每個隊列中記錄的card,并進行處理,大概實現邏輯如下:
1、根據 card 的地址,計算出 card 所在的 Region
2、若 Region 不存在,或 Region 是 Young 區,或該 Region 在回收集合中,則不進行處理
3、最終使用閉合函數 G1UpdateRSOrPushRefOopClosure::do_oop_nv() 的處理該 card 中的對象
refinement threads 線程數量可以通過 -XX:G1ConcRefinementThreads 或 -XX:ParallelGCThreads 參數設置
4.2 Collection Set
收集集合(CSet):一組可被回收的分區集合。在CSet中存活的數據會在GC過程中被移動到另一個可用分區,CSet中的分區可以來自eden空間、survivor空間、老年代
GC時在CSet中的所有存活數據都會被轉移,分區釋放回空閑分區隊列
4.3 PLAB
在 GC 線程的晉升本地分配緩沖區(PLAB)中,對象晉升到 survivor 分區或老年代分區
每個線程有獨立的PLAB,作用是避免多線程競爭相同數據
4.4 TLAB
JVM使用線程本地分配緩存TLAB 這種線程專屬的區間,來避免多線程沖突(無鎖方式),提高對象分配效率
TLAB 本身占用了 Eden 空間,即 JVM 會為每一個線程都分配一塊 TLAB 空間
5、G1 回收過程
大致流程:
年輕代回收時,G1 暫停所有應用程序線程,啟動多線程執行年輕代回收,然后移動存活對象到Survivor 或 O 區
一次只需要掃描/回收一小部分老年代的 Region 就可以
5.1 Young GC
- 回收時機:當 Eden 空間耗盡時,G1會啟動一次年輕代垃圾回收過程
- 回收對象:年輕代垃圾回收只會回收 Eden 區和 Survivor 區
回收流程:
- 第一階段,根掃描,根引用連同 RSet 記錄的外部引用作為掃描存活對象的入口
- 根是指 static 變量指向的對象,正在執行的方法調用鏈條上的局部變量等
- 掃描 remembered Set,看是否有老年代中的對象引用了新生代對象
- 第二階段,更新 RSet
- 處理 dirty card queue 中的 card,更新 RSet 后,可以準確反映老年代對所在的內存分段中對象的引用
- 第三階段,處理 RSet
- 識別被老年代對象指向的 Eden 中的對象,這些被指向的 Eden 中的對象被認為是存活的對象
- 第四階段,復制對象,遍歷對象樹
- Eden 區內存段中存活的對象會被復制到 Survivor 區中空的內存分段
- Survivor 區內存段中存活的對象如果年齡未達閾值,年齡會加1,達到閥值會被復制到 old 區中空的內存分段
- 若 Survivor 空間不夠,Eden 空間的部分數據會直接晉升到老年代空間
- 第五階段,處理引用,處理 Soft、Weak、Phantom、Final、JNI Weak 等引用
- 最終 Eden 空間的數據為空,GC停止工作,而目標內存中的對象都是連續存儲的,沒有碎片,所以復制過程可以達到內存整理的效果,減少碎片
5.2 Concurrent Marking
初始標記階段:標記從根節點直接可達的對象,這個階段是 STW 的,并且會觸發一次年輕代 GC
根區域掃描(Root Region Scanning):G1 GC 掃描 Survivor 區直接可達的老年代區域對象,并標記被引用的對象
這一過程必須在 young GC 之前完成(YoungGC時,會動 Survivor 區,所以這一過程必須在young GC之前完成)
并發標記(Concurrent Marking):在并發標記階段,若發現區域對象中的所有對象都是垃圾,則這個區域會被立即回收,此過程可能被 young GC 中斷
并發標記過程中,會計算每個區域的對象活性(區域中存活對象的比例)
再次標記(Remark):修正上一次的標記結果,是 STW
獨占清理(cleanup,STW):計算各個區域的存活對象和 GC 回收比例,并進行排序,識別可以混合回收的區域,是STW 的
這個階段并不會實際上去做垃圾的收集
并發清理階段:識別并清理完全空閑的區域
5.3 Mixed GC
- 觸發時機:老年代的堆占有率達到參數 -XX:InitiatingHeapOccupancyPercent 設定的值則觸發
- 回收對象:回收所有的 Young 和部分 Old(根據期望的GC停頓時間確定old區垃圾收集的優先順序)以及大對象區
- 回收過程:MixedGC 主要使用復制算法,把各個 region 中存活的對象拷貝到別的 region 里去,拷貝過程中若發現沒有足夠的空 region 能夠承載拷貝對象就會觸發一次 Full GC
- 并發標記結束以后,老年代中百分百為垃圾的內存分段被回收了,部分為垃圾的內存分段被計算了出來。默認情況下,這些老年代的內存分段會分8次(可以通過-XX:G1MixedGCCountTarget設置)被回收
- 混合回收的回收集(Collection Set)包括八分之一的老年代內存分段,Eden區 內存分段,Survivor區內存分段。 混合回收的算法和年輕代回收的算法完全一樣,只是回收集多了老年代的內存分段。具體過程請參考上面的年輕代回收過程。
- 由于老年代中的內存分段默認分8次回收,G1會優先回收垃圾多的內存分段。垃圾占內存分段比例越高的,越會被先回收。并且有一個閾值會決定內存分段是否被回收,-XX:G1MixedGCLiveThresholdPercent,默認為65%,意思是垃圾占內存分段比例要達到65%才會被回收。如果垃圾占比太低,意味著存活的對象占比高,在復制的時候會花費更多的時間
- 混合回收并不一定 要進行8次。有一個閾值**-XX :G1HeapWastePercent**,默認值為10%,意思是允許整個堆內存中有10%的空間被浪費,意味著如果發現可以回收的垃圾占堆內存的比例低于10%,則不再進行混合回收。因為GC會花費很多的時間但是回收到的內存卻很少
5.4 Full GC
- 特點:G1會停止應用程序的執行(Stop-The-World) ,使用單線程的內存回收算法進行垃圾回收,性能會非常差,應用程序停頓時間會很長。
- 導致 G1 Full GC 的原因可能有兩個: .
- 回收時,沒有足夠的 to-space 來存放晉升對象
- 并發處理過程沒完成空間就耗盡
總結
- 上一篇: 人工智能机器视觉专业英语积累
- 下一篇: 犹如“笼中困兽”的中国半导体,正在冒着敌