深入理解JVM垃圾收集機制(JDK1.8)

目錄

垃圾收集算法
標記-清除算法
複製算法
eden survivor複製過程概述
標記-整理算法
分代收集算法
HotSpot算法實現
垃圾收集器
Serial 收集器
ParNew 收集器
並行 Parallel
併發 Concurrent
Parallel Scavenge 收集器
Serial Old收集器
Parallel Old 收集器
CMS 收集器
CMS什麼時候FUll GC
1. 舊生代空間不足
2. Permanet Generation空間滿
3. CMS GC時出現promotion failed和concurrent mode failure
4. 統計得到的Minor GC晉升到舊生代的平均大小大於舊生代的剩餘空間
G1
什麼是垃圾回收
Java垃圾收集器的歷史
瞭解G1
對象分配策略
G1 Young GC
G1 Mix GC
三色標記算法
調優實踐
觸發Full GC
參考

垃圾收集算法

標記-清除算法

最基礎的收集算法是“標記-清除”(Mark-Sweep)算法,分兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象。

不足:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能導致以後在程序運行過程需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一個的垃圾收集動作。

複製算法

爲了解決效率問題,一種稱爲複製(Copying)的收集算法出現了,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊內存用完了,就將還存活着的對象複製到另外一塊上,然後再把已經使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。代價是內存縮小爲原來的一半。

商業虛擬機用這個回收算法來回收新生代。IBM研究表明98%的對象是“朝生夕死“,不需要按照1-1的比例來劃分內存空間,而是將內存分爲一塊較大的”Eden“空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活的對象一次性複製到另外一個Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。Hotspot虛擬機默認Eden和Survivor的比例是8-1.即每次可用整個新生代的90%, 只有一個survivor,即1/10被”浪費“。當然,98%的對象回收只是一般場景下的數據,我們沒有辦法保證每次回收都只有不多於10%的對象存活,當Survivor空間不夠時,需要依賴其他內存(老年代)進行分配擔保(Handle Promotion).

如果另外一塊survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接通過分配擔保機制進入老年代。

eden survivor複製過程概述

Eden Space字面意思是伊甸園,對象被創建的時候首先放到這個區域,進行垃圾回收後,不能被回收的對象被放入到空的survivor區域。

Survivor Space倖存者區,用於保存在eden space內存區域中經過垃圾回收後沒有被回收的對象。Survivor有兩個,分別爲To Survivor、 From Survivor,這個兩個區域的空間大小是一樣的。執行垃圾回收的時候Eden區域不能被回收的對象被放入到空的survivor(也就是To Survivor,同時Eden區域的內存會在垃圾回收的過程中全部釋放),另一個survivor(即From Survivor)裏不能被回收的對象也會被放入這個survivor(即To Survivor),然後To Survivor 和 From Survivor的標記會互換,始終保證一個survivor是空的。

爲啥需要兩個survivor?因爲需要一個完整的空間來複制過來。當滿的時候晉升。每次都往標記爲to的裏面放,然後互換,這時from已經被清空,可以當作to了。

標記-整理算法

複製收集算法在對象成活率較高時就要進行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以,老年代一般不能直接選用這種算法。

根據老年代的特點,有人提出一種”標記-整理“Mark-Compact算法,標記過程仍然和標記-清除一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理端邊界以外的內存.

分代收集算法

當前商業虛擬機的垃圾收集都採用”分代收集“(Generational Collection)算法,這種算法根據對象存活週期的不同將內存劃分爲幾塊。一般把Java堆分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。在新生代,每次垃圾收集時都發現大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。而老年代中因爲對象存活率較高,沒有額外的空間對它進行分配擔保,就必須使用”標記-清理“和”標記-整理“算法來進行回收。

HotSpot算法實現

在Java語言中,可作爲GC Roots的對象包括下面幾種:

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象
  • 方法去中類靜態屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中JNI(即一般說的Native方法)引用的對象

從可達性分析中從GC Roots節點找引用鏈這個操作爲例,可作爲GC Roots的節點主要在全局性的引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中,現在很多應用僅僅方法區就有數百兆,如果要逐個檢查裏面的引用,必然消耗很多時間。

可達性分析對執行時間的敏感還體現在GC停頓上,因爲這項分析工作必須在一個能確保一致性的快照中進行--這裏”一致性“的意思是指整個分析期間整個執行系統看起來就像被凍結在某個時間點,不可以出現分析過程中對象引用關係還在不斷變化的情況,該點不滿足的話分析結果準確性就無法得到保證。這點是導致GC進行時必須停頓所有Java執行線程(Sun公司將這件事情稱爲”Stop The World“)的一個重要原因,即使是在號稱(幾乎)不會發生停頓的CMS收集器中,枚舉根節點時也必須停頓的。

安全點,Safepoint

垃圾收集器

Serial 收集器

標記-複製。

單線程,一個CPU或一條收集線程去完成垃圾收集工作,收集時必須暫停其他所有的工作線程,直到它結束。

雖然如此,它依然是虛擬機運行在Client模式下的默認新生代收集器。簡單而高效。

ParNew 收集器

ParNew是Serial收集器的多線程版本。Server模式下默認新生代收集器,除了Serial收集器之外,只有它能與CMS收集器配合工作。

並行 Parallel

指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態。

併發 Concurrent

指用戶線程與垃圾收集線程同時執行(但不一定是並行的,可能會交替執行),用戶程序再繼續運行,而垃圾收集程序運行於另一個CPU上。

Parallel Scavenge 收集器

Parallel Scavenge 收集器是一個新生代收集器,它也是使用複製算法的收集器。看上去來ParNew一樣,有什麼特別?

Parallel Scavenge 收集器的特點是它的關注點與其他收集器不同,CMS等收集器關注點是儘可能縮短垃圾收集時用戶線程的停頓時間。而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用於運行用戶代碼的時間和CPU總小號時間的比值,即吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了100min,其中垃圾收集花費了1min,那吞吐量就是99%.

停頓時間越短就越適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗,而高吞吐量則可以高效地利用CPU時間,主要適合在後臺運算而不需要太多交互的任務。

Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間 -XX:MaxGCPauseMillis以及直接設置吞吐量大小的-XX:GCTimeRatio

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器。給Client模式下的虛擬機使用。

新生代採用複製算法,暫停所有用戶線程;

老年代採用標記-整理算法,暫停所有用戶線程;

Parallel Old 收集器

這裏注意,Parallel Scavage 收集器架構中本身有PS MarkSweep收集器來收集老年代,並非直接使用了Serial Old,但二者接近。本人win10 64位系統,jdk1.8.0_102,測試默認垃圾收集器爲:**PS MarkSweep **和 PS Scavenge。 也就是說Java8的默認並不是G1。

這是”吞吐量優先“,注重吞吐量以及CPU資源敏感的場合都可以優先考慮Parallel Scavenge和Parallel Old(PS Mark Sweep)。Java8 默認就是這個。

CMS 收集器

CMS(Concurrent Mark Sweep) 收集器是一種以獲取最短回收停頓時間爲目標的收集器。目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類尤其重視服務的響應速度,希望系統停頓時間最短。CMS收集器就非常符合這類應用的需求。

CMS基於 標記-清除算法實現。整個過程分爲4個步驟:

  1. 初始標記(CMS initial mark) -stop the world
  2. 併發標記(CMS concurrent mark)
  3. 重新標記(CMS remark) -stop the world
  4. 併發清除(CMS concurrent sweep)

初始標記,重新標記這兩個步驟仍然需要Stop The World, 初始標記僅僅標記以下GC Roots能直接關聯的對象,速度很快。

併發標記就是進行GC Roots Tracing的過程;

而重新標記階段則是爲了修正併發標記期間因爲用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄。這個階段停頓比初始標記稍微長,但遠比並發標記的時間短。

整個過程耗時最長的併發標記和併發清除過程,收集器都可以與用戶線程一起工作。總體上來說,CMS收集器的內存回收過程與用戶線程一起併發執行的。

CMS特點:併發收集,低停頓。

缺點

1.CMS收集器對CPU資源非常敏感。默認啓動的回收線程數是(CPU+3)/4. 當CPU 4個以上時,併發回收垃圾收集線程不少於25%的CPU資源。

2.CMS收集器無法處理浮動垃圾(Floating Garbage), 可能出現”Concurrent Mode Failure“失敗而導致另一次Full GC的產生。由於CMS併發清理時,用戶線程還在運行,伴隨產生新垃圾,而這一部分出現在標記之後,只能下次GC時再清理。這一部分垃圾就稱爲”浮動垃圾“。

由於CMS運行時還需要給用戶空間繼續運行,則不能等老年代幾乎被填滿再進行收集,需要預留一部分空間提供併發收集時,用戶程序運行。JDK1.6中,CMS啓動閾值爲92%. 若預留內存不夠用戶使用,則出現一次Concurent Mode Failure失敗。這時虛擬機啓動後備預案,臨時啓用Serial Old收集老年代,這樣停頓時間很長。

3.CMS基於”標記-清除“算法實現的,則會產生大量空間碎片,空間碎片過多時,沒有連續空間分配給大對象,不得不提前觸發一次FUll GC。當然可以開啓-XX:+UseCMSCompactAtFullCollection(默認開),在CMS頂不住要FullGC時開啓內存碎片合併整理過程。內存整理過程是無法併發的,空間碎片問題沒了,但停頓時間變長。

面試題:CMS一共會有幾次STW

首先,回答兩次,初始標記和重新標記需要。

然後,CMS併發的代價是預留空間給用戶,預留不足的時候觸發FUllGC,這時Serail Old會STW.

然後,CMS是標記-清除算法,導致空間碎片,則沒有連續空間分配大對象時,FUllGC, 而FUllGC會開始碎片整理, STW.

即2次或多次。

CMS什麼時候FUll GC

除直接調用System.gc外,觸發Full GC執行的情況有如下四種。

1. 舊生代空間不足

舊生代空間只有在新生代對象轉入及創建爲大對象、大數組時纔會出現不足的現象,當執行Full GC後空間仍然不足,則拋出如下錯誤:
java.lang.OutOfMemoryError: Java heap space
爲避免以上兩種狀況引起的FullGC,調優時應儘量做到讓對象在Minor GC階段被回收、讓對象在新生代多存活一段時間及不要創建過大的對象及數組。

2. Permanet Generation空間滿

PermanetGeneration中存放的爲一些class的信息等,當系統中要加載的類、反射的類和調用的方法較多時,Permanet Generation可能會被佔滿,在未配置爲採用CMS GC的情況下會執行Full GC。如果經過Full GC仍然回收不了,那麼JVM會拋出如下錯誤信息:
java.lang.OutOfMemoryError: PermGen space
爲避免Perm Gen佔滿造成Full GC現象,可採用的方法爲增大Perm Gen空間或轉爲使用CMS GC。

3. CMS GC時出現promotion failed和concurrent mode failure

對於採用CMS進行舊生代GC的程序而言,尤其要注意GC日誌中是否有promotion failed和concurrent mode failure兩種狀況,當這兩種狀況出現時可能會觸發Full GC。
promotionfailed是在進行Minor GC時,survivor space放不下、對象只能放入舊生代,而此時舊生代也放不下造成的;concurrent mode failure是在執行CMS GC的過程中同時有對象要放入舊生代,而此時舊生代空間不足造成的。
應對措施爲:增大survivorspace、舊生代空間或調低觸發併發GC的比率,但在JDK 5.0+、6.0+的版本中有可能會由於JDK的bug29導致CMS在remark完畢後很久才觸發sweeping動作。對於這種狀況,可通過設置-XX:CMSMaxAbortablePrecleanTime=5(單位爲ms)來避免。

4. 統計得到的Minor GC晉升到舊生代的平均大小大於舊生代的剩餘空間

這是一個較爲複雜的觸發情況,Hotspot爲了避免由於新生代對象晉升到舊生代導致舊生代空間不足的現象,在進行Minor GC時,做了一個判斷,如果之前統計所得到的Minor GC晉升到舊生代的平均大小大於舊生代的剩餘空間,那麼就直接觸發Full GC。
例如程序第一次觸發MinorGC後,有6MB的對象晉升到舊生代,那麼當下一次Minor GC發生時,首先檢查舊生代的剩餘空間是否大於6MB,如果小於6MB,則執行Full GC。
當新生代採用PSGC時,方式稍有不同,PS GC是在Minor GC後也會檢查,例如上面的例子中第一次Minor GC後,PS GC會檢查此時舊生代的剩餘空間是否大於6MB,如小於,則觸發對舊生代的回收。
除了以上4種狀況外,對於使用RMI來進行RPC或管理的Sun JDK應用而言,默認情況下會一小時執行一次Full GC。可通過在啓動時通過- java-Dsun.rmi.dgc.client.gcInterval=3600000來設置Full GC執行的間隔時間或通過-XX:+ DisableExplicitGC來禁止RMI調用System.gc。

G1

什麼是垃圾回收

首先,在瞭解G1之前,我們需要清楚的知道,垃圾回收是什麼?簡單的說垃圾回收就是回收內存中不再使用的對象。

垃圾回收的基本步驟

回收的步驟有2步:

1.查找內存中不再使用的對象

2.釋放這些對象佔用的內存

1,查找內存中不再使用的對象

那麼問題來了,如何判斷哪些對象不再被使用呢?我們也有2個方法:

1.引用計數法
引用計數法就是如果一個對象沒有被任何引用指向,則可視之爲垃圾。這種方法的缺點就是不能檢測到環的存在。

2.根搜索算法

根搜索算法的基本思路就是通過一系列名爲”GC Roots”的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。

現在我們已經知道如何找出垃圾對象了,如何把這些對象清理掉呢?

2. 釋放這些對象佔用的內存

常見的方式有複製或者直接清理,但是直接清理會存在內存碎片,於是就會產生了清理再壓縮的方式。

總得來說就產生了三種類型的回收算法。

1.標記-複製

2.標記-清理

3.標記-整理

基於分代的假設

由於對象的存活時間有長有短,所以對於存活時間長的對象,減少被gc的次數可以避免不必要的開銷。這樣我們就把內存分成新生代和老年代,新生代存放剛創建的和存活時間比較短的對象,老年代存放存活時間比較長的對象。這樣每次僅僅清理年輕代,老年代僅在必要時時再做清理可以極大的提高GC效率,節省GC時間。

Java垃圾收集器的歷史

第一階段,Serial(串行)收集器

在jdk1.3.1之前,java虛擬機僅僅能使用Serial收集器。 Serial收集器是一個單線程的收集器,但它的“單線程”的意義並不僅僅是說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。

PS:開啓Serial收集器的方式

-XX:+UseSerialGC

第二階段,Parallel(並行)收集器

Parallel收集器也稱吞吐量收集器,相比Serial收集器,Parallel最主要的優勢在於使用多線程去完成垃圾清理工作,這樣可以充分利用多核的特性,大幅降低gc時間。

PS:開啓Parallel收集器的方式

-XX:+UseParallelGC -XX:+UseParallelOldGC

第三階段,CMS(併發)收集器

CMS收集器在Minor GC時會暫停所有的應用線程,並以多線程的方式進行垃圾回收。在Full GC時不再暫停應用線程,而是使用若干個後臺線程定期的對老年代空間進行掃描,及時回收其中不再使用的對象。

PS:開啓CMS收集器的方式

-XX:+UseParNewGC -XX:+UseConcMarkSweepGC

第四階段,G1(併發)收集器

G1收集器(或者垃圾優先收集器)的設計初衷是爲了儘量縮短處理超大堆(大於4GB)時產生的停頓。相對於CMS的優勢而言是內存碎片的產生率大大降低。

PS:開啓G1收集器的方式

-XX:+UseG1GC

瞭解G1

G1的第一篇paper(附錄1)發表於2004年,在2012年纔在jdk1.7u4中可用。oracle官方計劃在jdk9中將G1變成默認的垃圾收集器,以替代CMS。爲何oracle要極力推薦G1呢,G1有哪些優點

首先,G1的設計原則就是簡單可行的性能調優

開發人員僅僅需要聲明以下參數即可:

-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200

其中-XX:+UseG1GC爲開啓G1垃圾收集器,-Xmx32g 設計堆內存的最大內存爲32G,-XX:MaxGCPauseMillis=200設置GC的最大暫停時間爲200ms。如果我們需要調優,在內存大小一定的情況下,我們只需要修改最大暫停時間即可。

其次,G1將新生代,老年代的物理空間劃分取消了。

這樣我們再也不用單獨的空間對每個代進行設置了,不用擔心每個代內存是否足夠。

取而代之的是,G1算法將堆劃分爲若干個區域(Region),它仍然屬於分代收集器。不過,這些區域的一部分包含新生代,新生代的垃圾收集依然採用暫停所有應用線程的方式,將存活對象拷貝到老年代或者Survivor空間。老年代也分成很多區域,G1收集器通過將對象從一個區域複製到另外一個區域,完成了清理工作。這就意味着,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會有cms內存碎片問題的存在了。

在G1中,還有一種特殊的區域,叫Humongous區域。 如果一個對象佔用的空間超過了分區容量50%以上,G1收集器就認爲這是一個巨型對象。這些巨型對象,默認直接會被分配在年老代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成負面影響。爲了解決這個問題,G1劃分了一個Humongous區,它用來專門存放巨型對象。如果一個H區裝不下一個巨型對象,那麼G1會尋找連續的H分區來存儲。爲了能找到連續的H區,有時候不得不啓動Full GC。

PS:在java 8中,持久代也移動到了普通的堆內存空間中,改爲元空間。

對象分配策略

說起大對象的分配,我們不得不談談對象的分配策略。它分爲3個階段:

1.TLAB(Thread Local Allocation Buffer)線程本地分配緩衝區
2.Eden區中分配
3.Humongous區分配

TLAB爲線程本地分配緩衝區,它的目的爲了使對象儘可能快的分配出來。如果對象在一個共享的空間中分配,我們需要採用一些同步機制來管理這些空間內的空閒空間指針。在Eden空間中,每一個線程都有一個固定的分區用於分配對象,即一個TLAB。分配對象時,線程之間不再需要進行任何的同步。

對TLAB空間中無法分配的對象,JVM會嘗試在Eden空間中進行分配。如果Eden空間無法容納該對象,就只能在老年代中進行分配空間。

最後,G1提供了兩種GC模式,Young GC和Mixed GC,兩種都是Stop The World(STW)的。下面我們將分別介紹一下這2種模式。

G1 Young GC

Young GC主要是對Eden區進行GC,它在Eden空間耗盡時會被觸發。在這種情況下,Eden空間的數據移動到Survivor空間中,如果Survivor空間不夠,Eden空間的部分數據會直接晉升到年老代空間。Survivor區的數據移動到新的Survivor區中,也有部分數據晉升到老年代空間中。最終Eden空間的數據爲空,GC停止工作,應用線程繼續執行。


這時,我們需要考慮一個問題,如果僅僅GC 新生代對象,我們如何找到所有的根對象呢? 老年代的所有對象都是根麼?那這樣掃描下來會耗費大量的時間。於是,G1引進了RSet的概念。它的全稱是Remembered Set,作用是跟蹤指向某個heap區內的對象引用。

在CMS中,也有RSet的概念,在老年代中有一塊區域用來記錄指向新生代的引用。這是一種point-out,在進行Young GC時,掃描根時,僅僅需要掃描這一塊區域,而不需要掃描整個老年代。

但在G1中,並沒有使用point-out,這是由於一個分區太小,分區數量太多,如果是用point-out的話,會造成大量的掃描浪費,有些根本不需要GC的分區引用也掃描了。於是G1中使用point-in來解決。point-in的意思是哪些分區引用了當前分區中的對象。這樣,僅僅將這些對象當做根來掃描就避免了無效的掃描。由於新生代有多個,那麼我們需要在新生代之間記錄引用嗎?這是不必要的,原因在於每次GC時,所有新生代都會被掃描,所以只需要記錄老年代到新生代之間的引用即可。

需要注意的是,如果引用的對象很多,賦值器需要對每個引用做處理,賦值器開銷會很大,爲了解決賦值器開銷這個問題,在G1 中又引入了另外一個概念,卡表(Card Table)。一個Card Table將一個分區在邏輯上劃分爲固定大小的連續區域,每個區域稱之爲卡。卡通常較小,介於128到512字節之間。Card Table通常爲字節數組,由Card的索引(即數組下標)來標識每個分區的空間地址。默認情況下,每個卡都未被引用。當一個地址空間被引用時,這個地址空間對應的數組索引的值被標記爲”0″,即標記爲髒被引用,此外RSet也將這個數組下標記錄下來。一般情況下,這個RSet其實是一個Hash Table,Key是別的Region的起始地址,Value是一個集合,裏面的元素是Card Table的Index。

Young GC 階段

階段1:根掃描

靜態和本地對象被掃描

階段2:更新RS

處理dirty card隊列更新RS

階段3:處理RS

檢測從年輕代指向年老代的對象

階段4:對象拷貝

拷貝存活的對象到survivor/old區域

階段5:處理引用隊列

軟引用,弱引用,虛引用處理

G1 Mix GC

Mix GC不僅進行正常的新生代垃圾收集,同時也回收部分後臺掃描線程標記的老年代分區。

它的GC步驟分2步:

1.全局併發標記(global concurrent marking)
2.拷貝存活對象(evacuation)

在進行Mix GC之前,會先進行global concurrent marking(全局併發標記)。 global concurrent marking的執行過程是怎樣的呢?

在G1 GC中,它主要是爲Mixed GC提供標記服務的,並不是一次GC過程的一個必須環節。global concurrent marking的執行過程分爲五個步驟:

初始標記(initial mark,STW)

在此階段,G1 GC 對根進行標記。該階段與常規的 (STW) 年輕代垃圾回收密切相關。

根區域掃描(root region scan

G1 GC 在初始標記的存活區掃描對老年代的引用,並標記被引用的對象。該階段與應用程序(非 STW)同時運行,並且只有完成該階段後,才能開始下一次 STW 年輕代垃圾回收。

併發標記(Concurrent Marking)

G1 GC 在整個堆中查找可訪問的(存活的)對象。該階段與應用程序同時運行,可以被 STW 年輕代垃圾回收中斷

最終標記(Remark,STW)

該階段是 STW 回收,幫助完成標記週期。G1 GC 清空 SATB 緩衝區,跟蹤未被訪問的存活對象,並執行引用處理。

清除垃圾(Cleanup,STW)

在這個最後階段,G1 GC 執行統計和 RSet 淨化的 STW 操作。在統計期間,G1 GC 會識別完全空閒的區域和可供進行混合垃圾回收的區域。清理階段在將空白區域重置並返回到空閒列表時爲部分併發。

三色標記算法

提到併發標記,我們不得不瞭解併發標記的三色標記算法。它是描述追蹤式回收器的一種有用的方法,利用它可以推演回收器的正確性。 首先,我們將對象分成三種類型的。

黑色:根對象,或者該對象與它的子對象都被掃描

灰色:對象本身被掃描,但還沒掃描完該對象中的子對象

白色:未被掃描對象,掃描完成所有對象之後,最終爲白色的爲不可達對象,即垃圾對象

當GC開始掃描對象時,按照如下圖步驟進行對象的掃描:

根對象被置爲黑色,子對象被置爲灰色。

繼續由灰色遍歷,將已掃描了子對象的對象置爲黑色。

遍歷了所有可達的對象後,所有可達的對象都變成了黑色。不可達的對象即爲白色,需要被清理。

這看起來很美好,但是如果在標記過程中,應用程序也在運行,那麼對象的指針就有可能改變。這樣的話,我們就會遇到一個問題:對象丟失問題

我們看下面一種情況,當垃圾收集器掃描到下面情況時:

這時候應用程序執行了以下操作:

A.c=C
B.c=null

這樣,對象的狀態圖變成如下情形:

這時候垃圾收集器再標記掃描的時候就會下圖成這樣:

很顯然,此時C是白色,被認爲是垃圾需要清理掉,顯然這是不合理的。那麼我們如何保證應用程序在運行的時候,GC標記的對象不丟失呢?有如下2中可行的方式:

1.在插入的時候記錄對象
2.在刪除的時候記錄對象

剛好這對應CMS和G1的2種不同實現方式:

在CMS採用的是增量更新(Incremental update),只要在寫屏障(write barrier)裏發現要有一個白對象的引用被賦值到一個黑對象 的字段裏,那就把這個白對象變成灰色的。即插入的時候記錄下來。

在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,刪除的時候記錄所有的對象,它有3個步驟:

1.在開始標記的時候生成一個快照圖標記存活對象

2.在併發標記的時候所有被改變的對象入隊(在write barrier裏把所有舊的引用所指向的對象都變成非白的)

3.可能存在遊離的垃圾,將在下次被收集

這樣,G1到現在可以知道哪些老的分區可回收垃圾最多。 當全局併發標記完成後,在某個時刻,就開始了Mix GC。這些垃圾回收被稱作“混合式”是因爲他們不僅僅進行正常的新生代垃圾收集,同時也回收部分後臺掃描線程標記的分區。混合式垃圾收集如下圖:

混合式GC也是採用的複製的清理策略,當GC完成後,會重新釋放空間。

調優實踐

MaxGCPauseMillis調優

前面介紹過使用GC的最基本的參數:

-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200

前面2個參數都好理解,後面這個MaxGCPauseMillis參數該怎麼配置呢?這個參數從字面的意思上看,就是允許的GC最大的暫停時間。G1儘量確保每次GC暫停的時間都在設置的MaxGCPauseMillis範圍內。 那G1是如何做到最大暫停時間的呢?這涉及到另一個概念,CSet(collection set)。它的意思是在一次垃圾收集器中被收集的區域集合。

Young GC:選定所有新生代裏的region。通過控制新生代的region個數來控制young GC的開銷。

Mixed GC:選定所有新生代裏的region,外加根據global concurrent marking統計得出收集收益高的若干老年代region。在用戶指定的開銷目標範圍內儘可能選擇收益高的老年代region。

在理解了這些後,我們再設置最大暫停時間就好辦了。 首先,我們能容忍的最大暫停時間是有一個限度的,我們需要在這個限度範圍內設置。但是應該設置的值是多少呢?我們需要在吞吐量跟MaxGCPauseMillis之間做一個平衡。如果MaxGCPauseMillis設置的過小,那麼GC就會頻繁,吞吐量就會下降。如果MaxGCPauseMillis設置的過大,應用程序暫停時間就會變長。G1的默認暫停時間是200毫秒,我們可以從這裏入手,調整合適的時間。

其他調優參數

-XX:G1HeapRegionSize=n

設置的 G1 區域的大小。值是 2 的冪,範圍是 1 MB 到 32 MB 之間。目標是根據最小的 Java 堆大小劃分出約 2048 個區域。

-XX:ParallelGCThreads=n

設置 STW 工作線程數的值。將 n 的值設置爲邏輯處理器的數量。n 的值與邏輯處理器的數量相同,最多爲 8。

如果邏輯處理器不止八個,則將 n 的值設置爲邏輯處理器數的 5/8 左右。這適用於大多數情況,除非是較大的 SPARC 系統,其中 n 的值可以是邏輯處理器數的 5/16 左右。

-XX:ConcGCThreads=n

設置並行標記的線程數。將 n 設置爲並行垃圾回收線程數 (ParallelGCThreads) 的 1/4 左右。

-XX:InitiatingHeapOccupancyPercent=45

設置觸發標記週期的 Java 堆佔用率閾值。默認佔用率是整個 Java 堆的 45%。

避免使用以下參數:

避免使用 -Xmn 選項或 -XX:NewRatio 等其他相關選項顯式設置年輕代大小。固定年輕代的大小會覆蓋暫停時間目標。

觸發Full GC

在某些情況下,G1觸發了Full GC,這時G1會退化使用Serial收集器來完成垃圾的清理工作,它僅僅使用單線程來完成GC工作,GC暫停時間將達到秒級別的。整個應用處於假死狀態,不能處理任何請求,我們的程序當然不希望看到這些。那麼發生Full GC的情況有哪些呢?

併發模式失敗

G1啓動標記週期,但在Mix GC之前,老年代就被填滿,這時候G1會放棄標記週期。這種情形下,需要增加堆大小,或者調整週期(例如增加線程數-XX:ConcGCThreads等)。

晉升失敗或者疏散失敗

G1在進行GC的時候沒有足夠的內存供存活對象或晉升對象使用,由此觸發了Full GC。可以在日誌中看到(to-space exhausted)或者(to-space overflow)。解決這種問題的方式是:

a. 增加 -XX:G1ReservePercent 選項的值(並相應增加總的堆大小),爲“目標空間”增加預留內存量。

b. 通過減少 -XX:InitiatingHeapOccupancyPercent 提前啓動標記週期。

c. 也可以通過增加 -XX:ConcGCThreads 選項的值來增加並行標記線程的數目。

巨型對象分配失敗

當巨型對象找不到合適的空間進行分配時,就會啓動Full GC,來釋放空間。這種情況下,應該避免分配大量的巨型對象,增加內存或者增大-XX:G1HeapRegionSize,使巨型對象不再是巨型對象。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章