JVM_06 垃圾收集器[ 三 ]
截止JDK1.8,一共有7款不同的垃圾收集器。
每一款不同的垃圾收集器都有不同的特點,在具體使用的時候,需要根據具體的情況選用不同的垃圾收集器
注意:標記算法需要維護一個空閑列表,復制和標記壓縮算法則使用指針碰撞
根據不同情況搭配垃圾回收器:
- 如果想要最小化地使用內存和并行開銷(串行),選擇Serial GC + Serial Old- 如果想要最大化應用程序的吞吐量,選擇parallel GC + Parallel Old (JDK1.8的默認選擇)(并行)- 如果想要最小化GC的中斷或停頓時間,選擇ParNewGC (并行)+ CMS GC(并行并發)一、 評估GC的性能指標 掌握
吞吐量:運行用戶代碼的時間占總運行時間的比例
(總運行時間:程序的運行時間 ? 內存回收的時間)
暫停時間:執行垃圾收集時,程序的工作線程被暫停的時間
內存占用: Java堆區所占的內存大小
垃圾收集開銷:吞吐量的補數,垃圾收集所用時間與總運行時間的比例。
收集頻率:相對于應用程序的執行,收集操作發生的頻率
(類似于大學洗衣服,天天洗,每次很快洗完;一周洗,洗很久)
(手機頻率高,垃圾線程所用時間短,吞吐量低)
說明:
- 這三者共同構成一個“不可能三角”。三者總體的表現會隨著技術進步而越來越好。一款優秀的收集器通常最多同時滿足其中的兩項。
- 這三項里,暫停時間的重要性日益凸顯。因為隨著硬件發展,內存占用多些越來越能容忍,硬件性能的提升也有助于降低收集器運行時對應用程序的影響,即提高了吞吐量。而內存的擴大,對延遲反而帶來負面效果。
- 簡單來說,主要抓住兩點:吞吐量、暫停時間
吞吐量
暫停時間
- 在設計(或使用) GC算法時,我們必須確定我們的目標: 一個GC算法只可能針對兩個目標之一(即只專注于較大吞吐量或最小暫停時間),或.嘗試找到一個二者的折衷
- 現在標準:在最大吞吐量優先的情況下,降低停頓時間。
二、不同的垃圾回收器概述
垃圾收集器發展史
7款經典的垃圾收集器
- 串行回收器:Serial、Serial Old
- 并行回收器:ParNew、Parallel Scavenge、 Parallel Old
- 并發回收器:CMS、G1
黃線為GC線程,藍線為用戶線程
GC發展過程:
7款經典的垃圾收集器與垃圾分代之間的關系
新生代收集器: Serial、 ParNeW、Parallel Scavenge
老年代收集器: Serial 0ld、 Parallel 0ld、 CMS
整堆收集器: G1(因為使用分區算法,所以能在新生代和老年代在起作用)
垃圾收集器的組合關系 超級重要
- 兩個收集器間有連線,表明它們可以搭配使用:Serial/Serial 01d、Serial/CMS、 ParNew/Serial 01d、ParNew/CMS、Parallel Scavenge/Serial 01d、Parallel Scavenge/Parallel 0ld、G1;
- 其中Serial 0ld作為CMS 出現"Concurrent Mode Failure"失敗的后備預案。
- (紅色虛線)由于維護和兼容性測試的成本,在JDK8時將Serial+CMS、ParNew+Serial 01d這兩個組合聲明為廢棄(JEP 173) ,并在JDK 9中完全取消了這些組合的支持(JEP214),即:移除。(綠色虛線)JDK 14中:棄用Parallel Scavenge和Serial0ld GC組合(JEP366 )(青色虛線)JDK 14中:刪除CMS垃圾回收器 (JEP 363)
(1). -xx:+PrintCommandLineFlags: 查看命令行相關參數(包含使用的垃圾收集器)
(2). 使用命令行指令: jinfo 一flag 相關垃圾回收器參數進程ID
( jinfo -flag UseParallelGC 進程id
jinfo -flag UseParallelOldGC 進程id )
三、Serial、SerialOld 回收器:串行回收
Serial收集器采用復制算法、串行回收和"Stop一 the一World"機制的方式執行內存回收
Serial 0ld收集器同樣也采用了串行回收 和"Stop the World"機制,只不過內存回收算法使用的是標記一壓縮算法
單線程回收:使用一個cpu或一條線程去完成垃圾收集工作 | 必須暫停其他所有的工作線程
使用 -XX: +UseSerialGC 參數可以指定年輕代和老年代都使用串行收集器
- 等價于新生代用Serial GC,且老年代用Serial 0ld GC
控制臺輸出:
-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseSerialGC
四、ParNew回收器:并行回收(了解)
如果說Serial GC是年輕代中的單線程垃圾收集器,那么ParNew收集器則是Serial收集器的多線程版本
ParNew收集器除了采用并行回收的方式執行內存回收外,兩款垃圾收集器之間幾乎沒有任何區別。ParNew收集器在年輕代中同樣也是采用復制算法、"Stop一the一World"機制
因為除Serial外,目前只有ParNew GC能與CMS收集器配合工作
在程序中,開發人員可以通過選項"-XX:+UseParNewGC"手動指定使用.ParNew收集器執行內存回收任務。它表示年輕代使用并行收集器,不影響老年代
-XX:ParallelGCThreads 限制線程數量,默認開啟和CPU數據相同的線程數。
五、Parallel回收器:吞吐量優先在Java8中,默認是此垃圾收集器
HotSpot的年輕代中除了擁有ParNew收集器是基于并行回收的以外, Parallel Scavenge收集器同樣也采用了復制算法、并行回收和"Stop the World"機制。
那么Parallel收集器的出現是否多此一舉?
- 和ParNew收集器不同,Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput),它也被稱為吞吐量優先的垃圾收集器。
- 自適應調節策略也是ParallelScavenge 與ParNew一個重要區別。
高吞吐量則可以高效率地利用CPU 時間,盡快完成程序的運算任務,主要適合在后臺運算而不需要太多交互的任務。因此,常見在服務器環境中使用。例如,那些執行批量處理、訂單處理、工資支付、科學計算的應用程序。
Parallel收集器在JDK1.6時提供了用于執行老年代垃圾收集的 Parallel 0ld收集器,用來代替老年代的Serial 0ld收集器
Parallel 0ld收集器采用了標記一壓縮算法,但同樣也是基于并行回收和”Stop一the一World"機制
在程序吞吐量優先的應用場景中,Parallel 收集器和Parallel 0ld收集器的組合,在Server模式下的內存回收性能很不錯
參數配置
六、CMS低延遲(jdk9中被廢棄、jdk14中被移除)
①. 在JDK1.5時期, HotSpot推出了一款在強交互應用中幾乎可認為有劃 時代意義的垃圾收集器: CMS (Concurrent
一Mark 一
Sweep)收集器,這款收集器是HotSpot虛擬機中第一款真正意義上的并發收集器,它第一次實現了讓垃圾收集線程與用戶線程同時工作。
②. CMS收集器的關注點是盡可能縮短垃圾收集時用戶線程的停頓時間。停頓時
間越短(低延遲)就越適合與用戶交互的程序,良好的響應速度能提升用戶體驗。
③. CMS的垃圾 收集算法采用標記一清除算法,并且也 會" stop一the一world"
④. 不幸的是,CMS 作為老年代的收集器,卻無法與JDK 1.4.0 中已經存在的新生代收集器Parallel
Scavenge配合工作,所以在JDK 1. 5中使用CMS來收集老年代的時候,新生代只能選擇ParNew或者Serial收集器中的一個
⑤. 在G1出現之前,CMS使用還是非常廣泛的。一直到今天,仍然有很多系統使用CMS GC
①. 初始標記(Initial一Mark)僅僅只是標記出和GCRoots能直接關聯到的對象,有stw現象
②. 并發標記(Concurrent一Mark)階段:從GC Roots的直接關聯對象開始遍歷整個對象圖的過程,這個過程耗時較長但是不需要停頓用戶線程,可以與垃圾收集線程一起并發運行
(并發標記階段有三色標記,下文有記錄)
③. 重新標記(Remark) 階段:有些對象可能開始是垃圾,在并發標記階段,由于用戶線程的影響,導致不是垃圾了,這里需要重新標記的是這部分對象,這個階段的停頓時間通常會比初始標記階段稍長一些,但也遠比并發標記階段的時間短
④. 并發清除:此階段清理刪除掉標記階段判斷的已經死亡的對象,釋放內存空間。由于不需要移動存活對象,所以這個階段也是可以與用戶線程同時并發的,注意:(并發清除階段會有浮動垃圾的產生)
什么是浮動垃圾:
remark過程標記活著的對象,從GCRoot的可達性判斷對象活著,但無法標記“死亡”的對象。如果在初始標記階段被標記為活著,并發運行過程中“死亡”,remark過程無法糾正,因此變為浮動垃圾,需等待下次gc的到來。
⑤. 注意:
①. 優點:并發收集、低延遲
②. CMS的弊端:
③.區分兩個注意事項:掌握
-XX:+UseConcMarkSweepGc 手動指定使用CMS收集器執行內存回收任務 (開啟該參數后會自動將一XX:+UseParNewGc打開。即: ParNew (Young區用) +CMS (0ld區用) +Serial 0ld的組合)
其他參數
①. 其實三色標記就是我們CMS在掃描過程中對對象的一種定義。那么具體的定義如下:
- 黑色(black):節點被遍歷完成,而且子節點都遍歷完成
- 灰色(gray): 當前正在遍歷的節點,而且子節點還沒有遍歷
- 白色(white):還沒有遍歷到的節點,即灰色節點的子節點
②. 根據三色掃描算法,如果有下面兩種情況發生,則會出現漏掃描的場景:
- 把一個白對象的引用存到黑對象的字段里,如果這個情況發生,因為標記為黑色的對象認為是掃描完成的,不會再對他進行掃描。只能通過灰色的對象(CMS垃圾收集器)
- 某個白對象失去了所有能從灰對象到達它的引用路徑(直接或間接)(g1垃圾收集器)
③. 三色過程:如下圖所示,假如說A引入了B,B引用了C,D沒有被任何引用。那么首先我們的CMS首先掃描到了A,發現A有引用B,那么我們的CMS會將A標記為黑色,B標記為灰色,然后這時候,通過B又找到了C那么這個時候發現C已經沒有任何引用了就會將C標記為黑色。但是我們的D到目前為止沒有被任何引用,記住我這里說的條件!!!那么D從始至終都沒有被掃描,此時就會一直是白色,對于白色的對象來說CMS在執行并發清理的時候就會將此類對象干掉。
④. 但是這里有了一個問題:如果我們的掃描過程已經結束這一段了,但是此時此刻我的A突然引用了D類型怎么辦,這樣一來我們的D只要被GC干掉是不是就會出現問題?也就是說我這里產生了一個漏標的問題。當然,我們的JVM開發人員可不是傻子,這里他們用了一個操作叫做增量更新和寫屏障來解決這種問題的。
①. 所謂增量更新就是在并發標記過程中,把賦值的這種新增的引用,做一個集合存起來。 在重新標記的時候會找到集合里面的引用然后重新去掃描,再把源頭標記為灰色。這就是我們的增量更新
②. 當然,在把我們新增的引用放到集合的時候,會實現一種寫屏障的方式。在對象前后通過一個dirty card queue將引用信息,存在card中,這個dirty card queue會放在cardtable中,而cardtable是記憶集的具體實現,最終這個引用就會放在記憶集中的 (寫屏障我們可以理解為在賦值操作的前面加一個方法,賦值的后面做一些操作,也可以理解為AOP。具體的C++實現代碼如下圖:)
①. 在剛剛我們再說寫屏障的時候提到了卡表,那么我們現在就來說說卡表是干什么用的。但是在說記憶集與卡表之前,我們要先知道what is 跨代引用~
②. 跨代引用:所謂跨代引用就是老年代的對象引用了新生代的對象,或者新生代的對象引用了老年代的對象。那對于這種情況我們的GC在進行掃描的時候不可能直接把我們的整個堆都掃描完,那這樣效率也太低了。所以這時候就需要開辟了一小塊空間,維護這種引用,而不必讓GC掃描整個堆區域。
③. 記憶集: 記憶集也叫rememberSet,垃圾收集器在新生代中建立了記憶集這樣的數據結構,用來避免把整個老年代加入到GC ROOTS的掃描范圍中。對于記憶集來說,我們可以理解為他是一個抽象類,那么具體實現它的方法將由子類去完成。這里我們簡單列舉一下實現記憶集的三種方式:
④. 卡表: 卡表(Car Table)是一種對記憶集的具體實現。主要定義了記憶集的記錄精度、與堆內存的映射關系等。卡表中的每一個元素都對應著一塊特定大小的內存塊,這個內存塊我們稱之為卡頁(card page),當存在跨帶引用的時候,它會將卡頁標記為dirty。那么JVM對于卡頁的維護也是通過寫屏障的方式,這也就是為什么剛剛我們跟進寫屏障操作到最后會發現它會對卡表進行一系列的操作。
-
①. satb 算法認為開始標記的都認為是活的對象,如上圖所示,引用B到D 的引用改為B到C時,通過write barrier寫屏障技術,會把B到D 的引用推到gc 遍歷執行的堆棧上,保證還可以遍歷到D對象,相對于d來說,引用從B–>A,SATB是從源入手解決的,即上面說的第2種情況, 這也能理解為啥叫satb 了,即認為開始時所有能遍歷到的對象都是需要標記的,即都認為是活的。如果我吧b = null,那么d 久是垃圾了, satb算法也還是會把D最終標記為黑色,導致D 在本輪gc 不能回收,成了浮動垃圾
-
②. Incremental Update
算法判斷如果一個白色的對象由一個黑色的對象引用,即目的,如上圖,D的引用由B–>A,A是目的地址,所以cms 的Incremental Update算法是從目標入手解決的,這是和SATB的第一個區別,發現這種情況時,也是通過write barrier寫屏障技術,把黑色的對象重新標記為灰色,讓collector 重新來掃描,活著通過mod-union table 來標記,cms 就是這樣實現的,這是第二個區別,做法不一樣,也是上面講的防止第一種情況發生
七、G1回收器 掌握(jdk9默認垃圾收集器)
-
①.G1是一個并行回收器,它把堆內存分割為很多不相關的區域(region物理上不連續),把堆分為2048個區域,每一個region的大小是1 -32M不等,必須是2的整數次冪。使用不同的region可以來表示Eden、幸存者0區、幸存者1區、老年代等
-
②. 每次根據允許的收集時間,優先回收價值最大的Region (每次回收完以后都有一個空閑的region,在后臺維護一個優先列表)
-
③. 由于這種方式的
①. 并行和并發
- 并行性: G1在回收期間,可以有多個Gc線程同時工作,有效利用多核計算能力。此時用戶線程STW
- 并發性: G1擁有與應用程序交替執行的能力,部分工作可以和應用程序同時執行,因此,一般來說,不會在整個回收階段發生完全阻塞應用程序的情況
②. 分代收集
- 從分代上看,G1依然屬于分代型垃圾回收器,他會區分年輕代和老年代,年輕代依然有Eden區和servivor區,但從堆結構上看,他不要求整個Eden區、你年輕代或者老年代都是連續的,也不再堅持固定大小和數量
- 將堆空間分為若干個區域(Region),這些區域中包含了邏輯上的年輕代和老年代,H用來存儲大對象
- 和之前的各類回收器不同,它同時兼顧年輕代和老年代0,對比其他回收器,或者工作在年輕代,或者工作在老年代
③. 空間整合
(G1將內存劃分為一個個的region。 內存的回收是以region作為基本單位的.Region之間是復制算法,但整體上實際可看作是標記一壓縮(Mark一Compact)算法,兩種算法都可以避免內存碎片。這種特性有利于程序長時間運行,分配大對象時不會因為無法找到連續內存空間而提前觸發下一次GC。尤其是當Java堆非常大的時候,G1的優勢更加明顯)
④. 可預測的停頓時間模型(即:軟實時soft real一time)
(可以通過參數-XX:MaxGCPauseMillis進行設置)
-
①. -XX:+UseG1GC 手動指定使用G1收集器執行內存回收任務
-
②. -XX:G1HeapRegionSize 設置每個Region的大小。值是2的冪,范圍是1MB到32MB之間,目標是根據最小的Java堆大小劃分出約2048個區域。默認是堆內存的1/2000
-
③. -XX:MaxGCPauseMillis 設置期望達到的最大Gc停頓時間指標(JVM會盡力實現,但不保證達到)。默認值是200ms(如果這個值設置很小,如20ms,那么它收集的region會少,這樣長時間后,堆內存會滿。產生FullGC,FullGC會出現STW,反而影響用戶體驗)
-
④. -XX:ParallelGCThread 設置stw.工作線程數的值。最多設置為8
-
⑤. -XX:ConcGCThreads設置并發標記的線程數。將n設置為并行垃圾回收線程數(ParallelGCThreads)的1/4左右
-
⑥. -XX:InitiatingHeapOccupancyPercent 設置觸發并發GC周期的Java堆占用率閾值。超過此值,就觸發GC。默認值是45
- G1的調優
-
①. 面向服務端應用,針對具有大內存,多處理器的機器
-
②. 最主要的應用時需要低GC延遲,并且有大堆的應用程序提供解決方案
-
①. 使用G1收集器時,它將整個Java堆劃分成約2048個大小相同的獨立Region塊,每個Region塊大小根據堆空間的實際大小而定,整體被控制在1MB到32MB之間,且為2的N次冪,即1MB,2MB, 4MB, 8MB, 1 6MB, 32MB。可以通過-XX:G1HeapRegionSize設定。所有的Region大小相同,且在JVM生命周期內不會被改變
-
②. 一個region 有可能屬于Eden, Survivor 或者0ld/Tenured 內存區域。但是一個region只可能屬于一個角色。圖中的E表示該region屬于Eden內存區域,s表示屬于Survivor內存區域,0表示屬于0ld內存區域。圖中空白的表示未使用的內存空間
-
③. 垃圾收集器還增加了一種新的內存區域,叫做Humongous內存區域,如圖中的H塊。主要用于存儲大對象,如果超過0.5個region-Size,就放到H (對于堆中的大對象,默認直接會被分配到老年代,但是如果它是一個短期存在的大對象,就會對垃圾收集器造成負面影響。為了解決這個問題,G1劃分了一個Humongous區,它用來專門存放大對象。如果一個H區裝不下一個大對象,那么G1會尋找連續的H區來存儲。為了能找到連續的H區,有時候不得不啟動FullGC。G1的大多數行為都把H區作為老年代的一部分來看待)
-
①. 問題:一個Region不可能是孤立的,一個Region中的對象可能被其他對象引用,如新生代中引用了老年代,這個時候垃圾回收時,會去掃描老年代,會出現STW
-
②. 解決:無論是G1還是分代收集器,JVM都是使用Remembered Set來避免全局掃描。每個Region都有 一個對應的Remembered Set;[下面過程需要掌握]
- 每次Reference類型數據寫操作時,都會產生一個Write Barrier
- 然后檢查將要寫入的引用指向的對象是否和該Reference類型數據在不同的Region (其他收集器:檢查老年代對象是否引用了新生代對象)
- 如果不同,通過CardTable把相關引用信息記錄到引用指向對象的所在Region對應的Remembered Set中;
- 當進行垃圾收集時,在GC根節點的枚舉范圍加入Remembered Set;就可以保證不進行全局掃描,也不會有遺漏
G1回收器垃圾回收過程
年輕代GC
-
①. 根掃描:一定要考慮remembered Set,看是否有老年代中的對象引用了新生代對象
-
②.更新RSet:處理dirty card queue(見備注)中的card,更新RSet。 此階段完成后,RSet可以準確的反映老年代對所在的內存分段中對象的引用 (dirty card queue:對于應用程序的引用賦值語句object.field=object,JVM會在之前和之后執行特殊的操作以在dirty card queue中入隊一個保存了對象引用信息的card。在年輕代回收的時候,G1會對Dirty CardQueue中所有的card進行處理,以更新RSet,保證RSet實時準確的反映引用關系。那為什么不在引用賦值語句處直接更新RSet呢?這是為了性能的需要,RSet的處理需要線程同步,開銷會很大,使用隊列性能會好很多)
-
③. 處理RSet:識別被老年代對象指向的Eden中的對象,這些被指向的Eden中的對象被認為是存活的對象
-
④. 復制對象:復制算法 (此階段,對象樹被遍歷,Eden區
內存段中存活的對象會被復制到Survivor區中空的內存分段,Survivor區內存段中存活的對象如果年齡未達閾值,年齡會加1,達到閥值會被會被復制到01d區中空的內存分段。如果Survivor空間不夠,Eden空間的部分數據會直接晉升到老年代空間) -
⑤. 處理引用: 處理Soft,Weak, Phantom, Final, JNI Weak等引用。最終Eden空間的數據為空,GC停止工作,而目標內存中的對象都是連續存儲的,沒有碎片,所以復制過程可以達到內存整理的效果,減少碎片
-
①. 初始標記階段:標記從根節點直接可達的對象。這個階段是STW的,并且會觸發一次年輕代GC
-
②. 根區域掃描(Root Region Scanning):G1GC掃描Survivor區直接可達的老年代區域對象,并標記被引用的對象。這一過程必須在young GC之前完成(YoungGC時,會動Survivor區,所以這一過程必須在young GC之前完成)
-
③. 并發標記(Concurrent Marking): 在整個堆中進行并發標記(和應用程序并發執行),此過程可能被young GC中斷。在并發標記階段,若發現區域對象中的所有對象都是垃圾,那這個區域會被立即回收。同時,并發標記過程中,會計算每個區域的對象活性(區域中存活對象的比例)。
-
④. 再次標記(Remark):由
于應用程序持續進行,需要修正上一次的標記結果。是STW的。G1中采用了比CMS更快的初始快照算法:snapshot一at一the一beginning(SATB) (在CMS中有詳細講解) -
⑤.獨占清理(cleanup,STW):計算各個區域的存活對象和GC回收比例,并進行排序,識別可以混合回收的區域。為下階段做鋪墊。是STW的。(這個階段并不會實際上去做垃圾的收集)
-
⑥. 并發清理階段:識別并清理完全空閑的區域
①. 當越來越多的對象晉升到老年代oldregion時,為了避免堆內存被耗盡,虛擬機會觸發一個混合的垃圾收集器,即Mixed GC, 該算法并不是一個0ldGC,除了回收整個Young Region,還會回收一部分的0ldRegion。這里需要注意:是一部分老年代, 而不是全部老年代。可以選擇哪些0ldRegion進行收集,從而可以對垃圾回收的耗時時間進行控制。也要注意的是Mixed GC并不是Fu1l GC
②. 其他說明:
③. Full GC
-
①. 堆內存過小,當G1在復制存活對象的時候沒有空的內存分段可用,則會回退到full gc,這種情況可以通過增大內存解決
-
②. 暫停時間-XX:MaxGCPauseMillis設置短,回收頻繁。由于用戶線程和GC線程一起執行,可能用戶線程產生的垃圾大于GC線程回收的垃圾,會導致內存不足,觸發Full
gc
④. 優化建議
-
①. 年輕代發送GC頻率高,避免使用-Xmn或-XX:NewRatio,讓JVM自己設置
-
②. 暫停時間目標不要太過嚴苛
八、 詳解-XX:+PrintGCDetails
①. Minor GC
②. Full GC
③. 舉例補充
/*** 在jdk7 和 jdk8中分別執行* -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC*/ public class GCLogTest1 {private static final int _1MB = 1024 * 1024;public static void testAllocation() {byte[] allocation1, allocation2, allocation3, allocation4;allocation1 = new byte[2 * _1MB];allocation2 = new byte[2 * _1MB];allocation3 = new byte[2 * _1MB];allocation4 = new byte[4 * _1MB];}public static void main(String[] agrs) {testAllocation();} }九、其他垃圾回收器概述
- ①. Open JDK12的Shenandoah GC
- 暫時時間短、吞吐量下降
- red hot 團隊
- ②. 革命性的ZGC(jdk11-oracle)
( 在盡可能對吞吐量影響不大的前提下.實現在任意堆內存大小都可以把垃圾收集的停頓時間控制在10毫秒之內)
總結
以上是生活随笔為你收集整理的JVM_06 垃圾收集器[ 三 ]的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一句话证明你是产品经理
- 下一篇: JVM_07 Class文件结构