通過(guò)介紹當(dāng)前的垃圾回收器和垃圾回收算法的對(duì)比和不同的優(yōu)勢(shì),來(lái)幫助讀者選擇適合自己的垃圾回收器。主要涉及對(duì)象存活的判斷、三種垃圾回收算法以及新生代和老年代的幾種傳統(tǒng)的收集器,最后會(huì)介紹一下比較火熱的Garbage First(G1)收集器。
JVM 內(nèi)存分配方式
在 Java 虛擬機(jī)需要一種方式來(lái)分配對(duì)象,就像操作系統(tǒng)需要管理內(nèi)存一樣,Java 虛擬機(jī)也需要一種方式來(lái)管理 Java 虛擬機(jī)的內(nèi)存,現(xiàn)在管理 Java 虛擬機(jī)的方式主要有兩種,一種被稱為指針碰撞(Bump the Point),另外一種被稱為空閑列表(Free List).
指針碰撞
這種內(nèi)存分配方式需要配合擁有內(nèi)存整理過(guò)程的垃圾收集器。簡(jiǎn)單來(lái)說(shuō),這種方式的內(nèi)存管理方式就是在空閑內(nèi)存和已用內(nèi)存之間設(shè)置一個(gè)指針,如果有新的內(nèi)存被分配,就需要將指針向空閑區(qū)域移動(dòng)對(duì)應(yīng)的大小。然而由于這種方式簡(jiǎn)單粗暴,就必須要對(duì)應(yīng)的垃圾收集器能夠?qū)⒋婊畹膶?duì)象占用的內(nèi)存整理在指針的一側(cè)。使用這種分配算法的垃圾收集器是Serial,ParNew等帶有內(nèi)存整理的垃圾收集器。
空閑列表
通過(guò)在虛擬機(jī)中維護(hù)一個(gè)列表來(lái)記錄那些內(nèi)存空間是能夠使用的。如果需要分配新的對(duì)象,將會(huì)在空閑的列表中尋找一個(gè)足夠大的空閑內(nèi)存來(lái)分配對(duì)象,然后更新這個(gè)空閑列表。采用標(biāo)記-清除算法的垃圾收集器(eg:CMS)會(huì)采用這種分配方式。但是這種分配方式很容易造成很多外部碎片,在一定程度上會(huì)造成內(nèi)存的浪費(fèi)。
如何定位需要訪問(wèn)的對(duì)象
如何通過(guò)變量表引用到需要用到的對(duì)象,通過(guò)引用我們要找到具體的對(duì)象,然后才能夠?qū)?duì)象做一些列的操作。引用方式主要有一下兩種方式
句柄方式

句柄方式通過(guò)在 Java 堆中維護(hù)一個(gè)句柄池,池中的維護(hù)了對(duì)象實(shí)例和對(duì)象類(lèi)型的引用,然后在根據(jù)這兩個(gè)信息分別找到對(duì)應(yīng)的存儲(chǔ)信息,但是這樣做的一個(gè)確定是實(shí)例對(duì)象的類(lèi)型和數(shù)據(jù)都是通過(guò)兩次尋址的方式來(lái)找到的。
直接引用

直接引用的方式相較于句柄的方式,在第一次尋址的時(shí)候就能夠找到對(duì)象的實(shí)例數(shù)據(jù),對(duì)象類(lèi)型數(shù)據(jù)的指針和對(duì)象示例放到了一起,從而一次尋址就能夠找到對(duì)象的示例數(shù)據(jù)。
垃圾回收算法
介紹常用的垃圾回收算法,針對(duì)HotSpot虛擬機(jī)做一些深入的介紹。
Mark-Sweep算法
標(biāo)記-清除(Mark-Sweep)算法回收前后內(nèi)存占用情況如下:

標(biāo)記-清除(Mark-Sweep)算法是通過(guò)可以回收的內(nèi)存進(jìn)行標(biāo)記后做清除操作,這樣做的缺點(diǎn)就是會(huì)產(chǎn)生很多的內(nèi)存碎片,可能的結(jié)果就是空閑內(nèi)存總量是能夠進(jìn)行新對(duì)象的分配的,但是由于這些空閑的空間都不連續(xù),一個(gè)對(duì)象放不下。就必須進(jìn)行另外一次垃圾回收,垃圾回收又回引起服務(wù)的暫時(shí)停頓。
Mark-Compact 算法
標(biāo)記-整理(Mark-Compact)算法回收前后內(nèi)存占用情況如下:

標(biāo)記-整理(Mark-Compact)算法解決了標(biāo)記-清除(Mark-Sweep)算法會(huì)產(chǎn)生很多不連續(xù)的空閑空間做出了改進(jìn),具體的方法就是在進(jìn)行標(biāo)記之后不是將對(duì)象直接清理掉,而是將存活的對(duì)象進(jìn)行整理,使得存活的對(duì)象占用一塊連續(xù)的內(nèi)存空間,因?yàn)榇婊顚?duì)象和可回收空間有明顯的分割,所以可以直接對(duì)邊界之外的內(nèi)存進(jìn)行直接的清理。這樣的清理無(wú)疑更加有效率,雖然增加了移動(dòng)對(duì)象的開(kāi)支,但是整體上會(huì)比標(biāo)記-清除(Mark-Sweep)算法更有效率。
Copying 算法
復(fù)制(Copying)算法回收前后內(nèi)存占用情況如下:

復(fù)制(Copying)算法將內(nèi)存區(qū)域分為大小相同的兩個(gè)區(qū)域,當(dāng)需要內(nèi)存回收的時(shí)候,只需要將存活的對(duì)象復(fù)制到另外一塊空閑區(qū)域,然后將原來(lái)的區(qū)域一次清理干凈,什么都不留。相較于標(biāo)記-清除(Mark-Sweep)算法的優(yōu)勢(shì)就是在解決了在標(biāo)記和清除階段的低效率和內(nèi)存碎片問(wèn)題,相較于標(biāo)記-整理(Mark-Compact)算法的優(yōu)勢(shì)則是有著固定的內(nèi)存分割線,而不是動(dòng)態(tài)的調(diào)整,同時(shí)由于有兩塊相同大小的內(nèi)存區(qū)域,存活的對(duì)象的復(fù)制和內(nèi)存的回收會(huì)更有效率。缺點(diǎn)也十分明顯,就是需要的內(nèi)存空間是可用空間的2倍,內(nèi)存的使用率永遠(yuǎn)不會(huì)超過(guò)50%。(現(xiàn)代虛擬機(jī)根據(jù)經(jīng)驗(yàn)優(yōu)化了這個(gè)比例,在后面介紹收集器的時(shí)候會(huì)介紹)
聊聊HotSpot
HotSpot 是現(xiàn)在最廣泛使用的虛擬機(jī),擁有出色的性能。
判斷對(duì)象是否存活
目前針對(duì)對(duì)象的判斷生存狀態(tài)的的算法主要有引用計(jì)數(shù)法和可達(dá)性分析兩種。在Object-C等語(yǔ)言中使用的是引用計(jì)數(shù)法,在HotSpot中,是使用可達(dá)性分析來(lái)判斷的。
引用計(jì)數(shù)法(Reference Counting)
引用計(jì)數(shù)法是想要解決引用問(wèn)題最容易想到的一種算法,既然需要知道對(duì)象是否存活,那就在對(duì)象上加個(gè)計(jì)數(shù)器來(lái)表示自己被引用的次數(shù)唄,每有一次新的引用,這個(gè)計(jì)數(shù)器就加1,每少一次引用,計(jì)數(shù)器就減1。這樣看來(lái),事情似乎出奇的簡(jiǎn)單,因?yàn)楫?dāng)該對(duì)象的計(jì)數(shù)器為0的時(shí)候,這就是這個(gè)對(duì)象的死期了。這種方式十分高效,而且簡(jiǎn)單。

<center>引用計(jì)數(shù)方法對(duì)象的的數(shù)據(jù)結(jié)構(gòu)</center>
事情到此看起來(lái)都很美好。然而,卻有一種循環(huán)引用的問(wèn)題讓想使用這種算法的 GC 退縮。那便是循環(huán)引用問(wèn)題,像下圖這種,兩個(gè)對(duì)象相互引用,但是沒(méi)有其他對(duì)象引用這兩個(gè)對(duì)象的情況,實(shí)際上這兩個(gè)對(duì)象已經(jīng)都沒(méi)有利用價(jià)值了,應(yīng)該讓 GC 回收??墒怯捎趦蓚€(gè)對(duì)象互相都持有對(duì)方的一個(gè)引用,讓 GC 以為他們都還需要繼續(xù)生存以服務(wù)另外的對(duì)象。長(zhǎng)期下去會(huì)造成很?chē)?yán)重的內(nèi)存泄露。

<center>循環(huán)引用對(duì)象</center>
除了上面這種最簡(jiǎn)單的引用計(jì)數(shù)法,還有延遲引用計(jì)數(shù)法和Sticky引用計(jì)數(shù)法。這里不再深入討論,可以參考 GC引用計(jì)數(shù)算法 ,或者直接閱讀《垃圾回收的算法與實(shí)現(xiàn)》。
可達(dá)性分析(Reachablity Analysis)
與引用計(jì)數(shù)法相比,可達(dá)性分析就顯得更加復(fù)雜了??蛇_(dá)性分析是通過(guò)GC Root開(kāi)始搜索整個(gè)對(duì)象池,如果某些對(duì)象無(wú)法搜索到,在圖論中我們稱之為不可達(dá),那么這些不可達(dá)的對(duì)象就會(huì)成為GC的刀下鬼了。通過(guò)這種方式就能夠?qū)崿F(xiàn)回收循環(huán)引用的對(duì)象。
[圖片上傳失敗...(image-10d21e-1516334222313)]
<center>可達(dá)性分析判斷對(duì)象是否可回收</center>
在HotSpot中GC Root主要有
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象。
- 方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象。
- 方法區(qū)中常量引用的對(duì)象
- 本地方法棧中
JNI(即一般說(shuō)的Native方法)引用的對(duì)象
HotSpot 的可達(dá)性分析實(shí)現(xiàn)
在使用可達(dá)性分析算法的時(shí)候,不可避免地會(huì)引起GC 停頓,因?yàn)楸仨毐WC在進(jìn)行可達(dá)性分析的時(shí)候?qū)ο笾g的引用關(guān)系是不變的,我們無(wú)法針對(duì)一個(gè)在不斷變化的圖分析他的可達(dá)性。因此在進(jìn)行可達(dá)性分析的時(shí)候難免要停止所有的用戶線程。我們通常稱之為STW(Stop The Word). 有些收集器通過(guò)一些手段可以減少STW的時(shí)間(例如CMS),但是無(wú)法跳過(guò)這個(gè)過(guò)程。
目前主流的 Java 虛擬機(jī)使用的都是準(zhǔn)確式GC,就是說(shuō)當(dāng)STW發(fā)生的時(shí)候,并不需要檢查全部的變量從而確定那些變量是引用類(lèi)型,而是在類(lèi)加載完成之后會(huì)將引用類(lèi)型的變量存在一個(gè)數(shù)據(jù)結(jié)構(gòu)中,在HotSpot虛擬機(jī)中使用OopMap來(lái)記錄。
準(zhǔn)確式GC解決了引用類(lèi)型的定位問(wèn)題,但是引用類(lèi)型之間的關(guān)系錯(cuò)綜復(fù)雜,如果為每一條指令都生成對(duì)應(yīng)OopMap,那么GC的空間成本就會(huì)很大。為了解決這問(wèn)題,引入了安全點(diǎn)(Safepoint)和安全區(qū)域(Safe Region)兩個(gè)概念。HotSpot只有在安全點(diǎn)(Safepoint)和安全區(qū)域(Safe Region)這兩個(gè)位置才會(huì)生成相應(yīng)的OopMap,這兩個(gè)區(qū)域的選擇標(biāo)準(zhǔn)是能夠讓程序長(zhǎng)時(shí)間執(zhí)行(也就是這段時(shí)間內(nèi)不會(huì)發(fā)生引用關(guān)系的改變)的指令。
安全點(diǎn)一般設(shè)置在指令復(fù)用的代碼附近,因?yàn)檫@些代碼比較符合“長(zhǎng)時(shí)間執(zhí)行”的條件,這些代碼的典型代表就是方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等。如何讓程序在安全點(diǎn)上停下里等待GC也有兩種方式,分別是搶先式中斷(Preemptive Suspension)和主動(dòng)式中斷(Voluntary Suspension). 搶先式中斷不需要程序配合,當(dāng) GC 發(fā)生時(shí)會(huì)中斷所有的線程,然后找出不在安全點(diǎn)上的線程讓其繼續(xù)執(zhí)行到最近的安全點(diǎn);主動(dòng)式中斷需要程序進(jìn)行配合,設(shè)置一個(gè)標(biāo)識(shí)位,當(dāng)GC開(kāi)始后這個(gè)標(biāo)識(shí)位置位,所有的進(jìn)程到達(dá)安全點(diǎn)之后會(huì)主動(dòng)的去查詢這個(gè)標(biāo)識(shí)位,如果為真時(shí)就會(huì)自動(dòng)中斷掛起自己。
安全區(qū)域是配合安全點(diǎn)的一種機(jī)制,因?yàn)橛行┲噶畛绦蚴恰安粓?zhí)行”的,例如線程處在 Sleep 狀態(tài)或者 Blocked 狀態(tài),如果在 GC 的時(shí)候一直等待進(jìn)入安全點(diǎn),會(huì)使 GC 停頓變得十分長(zhǎng)。 當(dāng)線程執(zhí)行到安全區(qū)域的開(kāi)始位置的時(shí)候,會(huì)標(biāo)識(shí)自己已經(jīng)進(jìn)入安全區(qū)域了,而在離開(kāi)的時(shí)候會(huì)主動(dòng)檢查系統(tǒng)是否已經(jīng)完成了根結(jié)點(diǎn)的枚舉,如果沒(méi)有就繼續(xù)等待,如果完成當(dāng)前線程才可以繼續(xù)執(zhí)行后面的代碼。
垃圾收集器簡(jiǎn)介
按照使用場(chǎng)景介紹常用的垃圾收集器,介紹他們的適用場(chǎng)景。讓讀者能夠根據(jù)自己的應(yīng)用場(chǎng)景來(lái)選擇合適的收集器。

<center>垃圾收集器組合和分類(lèi)</center>
上圖中展示了各種收集器是新生代收集器還是老年代收集器,如果上面兩種收集器時(shí)間有連線,則表示可以配合使用。具體的GC配置參數(shù)可以參照 3. 常用垃圾收集器參數(shù)
首先說(shuō)明兩個(gè)名次的含義,以免在下面的閱讀中產(chǎn)生歧義:
-
并行(Parallel):指多條垃圾收集線程并行工作,但此時(shí)用戶線程仍然處于等待狀態(tài) -
并發(fā)(Concurrent):用戶線程與垃圾收集線程同時(shí)執(zhí)行(并行執(zhí)行,或者交替執(zhí)行)
新生代收集器
本文介紹的新生代收集器都是使用的復(fù)制算法
Serial 收集器
串行垃圾收集器,說(shuō)明他是單線程工作的,他只會(huì)使用一個(gè)CPU,使用一個(gè)線程去完成垃圾回收工作,同時(shí)這也意味著當(dāng)Serial GC進(jìn)行垃圾回收的時(shí)候,用戶線程也不得不暫停(STW)。Serial 收集器+Serial Old 收集器垃圾回收的時(shí)間線如下圖所示:

這樣的用戶線程對(duì)于現(xiàn)在的服務(wù)來(lái)說(shuō)基本是不可接受的,減少 GC 停頓也一直是很多垃圾收集器的努力目標(biāo)??此剖帧盁o(wú)能”的
Serial GC只能在客戶端中發(fā)揮作用,因?yàn)橐话憧蛻舳藨?yīng)用需要的內(nèi)存并不是很大,因此停頓幾十毫秒就可以完成內(nèi)存回收,這樣也是可以接受了,同時(shí)省去了并行 GC 切換線程的開(kāi)銷(xiāo)。
ParNew 收集器
ParNew 收集器基本上就是Serial 收集器的升級(jí)版本,在新生代中收集的時(shí)候采用了并行的方案。其他部分與Serial 收集器基本一樣。該收集器在單 CPU 的環(huán)境下并不會(huì)有優(yōu)于Serial的表現(xiàn),,反而會(huì)由于線程的頻繁切換而降低性能,而對(duì)于多核CPU就會(huì)有較好的表現(xiàn)了。因此比較適合服務(wù)端程序。ParNew 收集器+Serial Old 收集器的 GC 過(guò)程如下圖所示:
[圖片上傳失敗...(image-eacfc3-1516334222313)]
雖然ParNew 收集器只是針對(duì)Serial 收集器的簡(jiǎn)單升級(jí),但是有著十分廣泛的使用,原因就是能和CMS收集器搭配使用的只有Serial 收集器和ParNew 收集器兩種。CMS的廣泛使用就帶來(lái)了ParNew的廣泛使用。
Parallel Scavenge 收集器
Parallel Scavenge 收集器也是一個(gè)新生代收集器,他也是并行的、使用復(fù)制算法的。和ParNew收集器非常相似,GC過(guò)程圖也與ParNew收集器相同。然而該收集器的關(guān)注點(diǎn)與其他收集器不同,其他收集器關(guān)注點(diǎn)都是如何縮短GC停頓時(shí)間,而Parallel Scavenge 收集器關(guān)注的確實(shí)如何保持系統(tǒng)一個(gè)較高的吞吐量(吞吐量 = 運(yùn)行用戶代碼的時(shí)間/(運(yùn)行用戶代碼時(shí)間+垃圾收集時(shí)間))。因此該收集器主要適合那些交互較少,運(yùn)算較多的服務(wù)。該收集器允許用戶設(shè)置極少的參數(shù)就能保證一個(gè)較高的吞吐量。
該收集器提供兩個(gè)參數(shù)用于精確控制吞吐量-XX:MaxGCPauseMillis和-XX:GCTimeRatio兩個(gè)參數(shù),含義分別是最大GC停頓時(shí)間和直接設(shè)置吞吐量。GC的處理優(yōu)先級(jí)是MaxGCPauseMillis最高,GCTimeRatio次之,其他的空間大小配置優(yōu)先級(jí)最低。除了上面的兩個(gè)參數(shù)之外,還有一個(gè)參數(shù)-XX:+UseAdaptiveSizePolicy,當(dāng)設(shè)置了這個(gè)參數(shù)之后,就不需要再配置新生代的大小、Eden 和 Survivor 區(qū)域的比例、晉升老年代對(duì)象年齡等參數(shù)(設(shè)置了也沒(méi)用),虛擬機(jī)會(huì)根據(jù)當(dāng)前系統(tǒng)運(yùn)行狀況來(lái)動(dòng)態(tài)調(diào)整上面的幾個(gè)參數(shù)。
Parallel Scavenge(-XX:+UseParallelGC)框架下,默認(rèn)是在要觸發(fā)full GC前先執(zhí)行一次young GC,并且兩次GC之間能讓?xiě)?yīng)用程序稍微運(yùn)行一小下,以期降低full GC的暫停時(shí)間(因?yàn)閥oung GC會(huì)盡量清理了young gen的死對(duì)象,減少了full GC的工作量)??刂七@個(gè)行為的VM參數(shù)是-XX:+ScavengeBeforeFullGC
老年代收集器
Serial Old 收集器
Serial Old 收集器就是Serial 收集器的老年代版本,主要也是給Client模式下的虛擬機(jī)使用。在 Server 應(yīng)用中用于和 PS 收集器搭配使用,還有就是在 CMS 失敗后,作為后備方案來(lái)進(jìn)行垃圾收集。

Parallel Old 收集器
Parallel Old 收集器是在JDK1.6中推出的,在此之前Parallel Scavenge 收集器只能和Serial Old 收集器搭配使用,由于Serial Old 收集器的拖累,導(dǎo)致高吞吐量的優(yōu)勢(shì)無(wú)法體現(xiàn)出來(lái),這種組合的吞度量甚至不如ParNew 收集器+CMS收集器的組合。而Parallel Old 收集器的正式推出表示在需要關(guān)注高吞吐量的服務(wù)中可以使用Parallel Scavenge 收集器+Parallel Old 收集器的組合。這種組合的 GC 回收過(guò)程線程運(yùn)行情況如下圖:
[圖片上傳失敗...(image-62716-1516334222313)]
Concurrent Mark Sweep 收集器
CMS 收集器收集器是以最短的停頓時(shí)間為目標(biāo)的收集器,這樣的目標(biāo)可以保證服務(wù)的高可用性,因此也成為了各種以交互為目的的服務(wù)的首選收集器。該收集器采用的是標(biāo)記-清除算法,而前面介紹的收集器都是采用的標(biāo)記-整理算法。該收集器 GC 過(guò)程相對(duì)復(fù)雜一些。分為以下四個(gè)步驟:
- 初始標(biāo)記(CMS initial mark)
- 并發(fā)標(biāo)記(CMS concurrent mark)
- 重新標(biāo)記(CMS final remark)
- 并發(fā)清除(CMS concurrent sweep)
- 重置線程(CMS concurrent reset)
這里截取一段 GC 日志
2017-11-07T16:20:41.582+0800: 2.119: [GC (CMS Initial Mark) [1 CMS-initial-mark: 0K(3670016K)] 26854K(4141888K), 0.0036737 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2017-11-07T16:20:41.585+0800: 2.123: [CMS-concurrent-mark-start]
2017-11-07T16:20:41.587+0800: 2.125: [CMS-concurrent-mark: 0.002/0.002 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
2017-11-07T16:20:41.587+0800: 2.125: [CMS-concurrent-preclean-start]
2017-11-07T16:20:41.595+0800: 2.133: [CMS-concurrent-preclean: 0.008/0.008 secs] [Times: user=0.05 sys=0.00, real=0.01 secs]
2017-11-07T16:20:41.595+0800: 2.133: [CMS-concurrent-abortable-preclean-start]
2017-11-07T16:20:43.996+0800: 4.534: [CMS-concurrent-abortable-preclean: 1.514/2.401 secs] [Times: user=5.75 sys=0.28, real=2.40 secs]
2017-11-07T16:20:43.996+0800: 4.534: [GC (CMS Final Remark) [YG occupancy: 241272 K (471872 K)]4.534: [Rescan (parallel) , 0.0123556 secs]4.546: [weak refs processing, 0.0000203 secs]4.546: [class unloading, 0.0059852 secs]4.552: [scrub symbol table, 0.0055144 secs]4.558: [scrub string table, 0.0006434 secs][1 CMS-remark: 0K(3670016K)] 241272K(4141888K), 0.0260691 secs] [Times: user=0.11 sys=0.00, real=0.02 secs]
2017-11-07T16:20:44.023+0800: 4.560: [CMS-concurrent-sweep-start]
2017-11-07T16:20:44.023+0800: 4.560: [CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2017-11-07T16:20:44.023+0800: 4.560: [CMS-concurrent-reset-start]
2017-11-07T16:20:44.041+0800: 4.578: [CMS-concurrent-reset: 0.018/0.018 secs] [Times: user=0.07 sys=0.02, real=0.02 secs]
real是程序的實(shí)際運(yùn)行時(shí)間,sys是內(nèi)核態(tài)的時(shí)間,user是用戶態(tài)的時(shí)間,單核情況,real遠(yuǎn)遠(yuǎn)大于user和sys之和。real,從程序開(kāi)始到程序執(zhí)行結(jié)束時(shí)所消耗的時(shí)間,包括CPU的用時(shí)和所有延遲程序執(zhí)行的因素的總和。CPU用時(shí)被劃分為user和sys兩塊。user表示程序本身,以及它所調(diào)用的庫(kù)中的子例程使用的時(shí)間。sys是由程序直接或間接調(diào)用的系統(tǒng)調(diào)用執(zhí)行的時(shí)間。
real=cpu用時(shí)+其他因素時(shí)間,cpu 用時(shí)=user+sys,所以: real> user + sys (單核情況)
GC 過(guò)程中各個(gè)階段 GC 線程和用戶線程的運(yùn)行關(guān)系如下圖:
[圖片上傳失敗...(image-d17a24-1516334222313)]
初始標(biāo)記、重新標(biāo)記這兩個(gè)步驟需要 STW, 初始標(biāo)記僅僅是標(biāo)記以下 GC Roots 能夠直接關(guān)聯(lián)到的對(duì)象,速度很快,并發(fā)標(biāo)記階段就是進(jìn)行GC RootsTracing的過(guò)程。重新標(biāo)記是為了修正并發(fā)標(biāo)記期間由于用戶線程還在繼續(xù)執(zhí)行而產(chǎn)生變動(dòng)的那一部分對(duì)象,這個(gè)階段相比初始標(biāo)記要長(zhǎng)一點(diǎn),但是遠(yuǎn)比并發(fā)標(biāo)記短。整體來(lái)看用時(shí)最多的幾個(gè)階段:并發(fā)標(biāo)記、并發(fā)清理、重置線程都會(huì)都是并發(fā)的,這樣可以讓?xiě)?yīng)用程序盡可能的減少停頓。
【關(guān)于CMS-concurrent-abortable-preclean】:從日志中我們還發(fā)現(xiàn)了一個(gè)細(xì)節(jié)叫做CMS-concurrent-abortable-preclean,這就要從Concurrent precleaning階段說(shuō)起了。Concurrent precleaning階段的實(shí)際行為是:針對(duì)新生代做抽樣,等待新生代在某個(gè)時(shí)間段(默認(rèn)5秒,可以通過(guò)CMSMaxAbortablePrecleanTime參數(shù)設(shè)置)執(zhí)行一次Minor GC,如果這個(gè)時(shí)間段內(nèi)GC沒(méi)有發(fā)生,那么就繼續(xù)進(jìn)行下一階段(Remark);如果時(shí)間段內(nèi)觸發(fā)了Minor GC,則可能會(huì)執(zhí)行一些優(yōu)化(具體可以參考https://blogs.oracle.com/jonthecollector/entry/did_you_know)
CMS 收集器也有明顯的幾個(gè)缺點(diǎn):一是 CMS 默認(rèn)啟動(dòng)的回收線程數(shù)是(CPU數(shù)量+3)/4,也就是當(dāng) CPU 在 4 個(gè)以上時(shí),并發(fā)垃圾回收時(shí) GC線程占用不少于 25% 的 CPU 資源,當(dāng) CPU 不足 4 個(gè)時(shí),情況就變得更加嚴(yán)峻了。第二個(gè)缺點(diǎn)就是 CMS 無(wú)法處理浮動(dòng)垃圾(Floating Garbage),浮動(dòng)垃圾是指在并發(fā)清理階段新產(chǎn)生的垃圾,由于 CMS 垃圾回收線程要和用戶線程并發(fā),因此必須要保留一部分內(nèi)存在回收期間供用戶線程使用,增額比例通過(guò)參數(shù)CMSInitiatingOccupancyFraction設(shè)置,表示老年代空間占用比例達(dá)到多少的時(shí)候會(huì)出發(fā)CMS GC,這個(gè)值在JDK1.6中默認(rèn)值是68,在JDK1.7和JDK1.8中都是92。如果在 CMS GC 期間剩余的老年代內(nèi)存空間不足與支持程序繼續(xù)執(zhí)行,就是觸發(fā)GC降級(jí),也就是Concurrent Mode Failure,這個(gè)時(shí)候就需要使用Serial Old收集器來(lái)完成老年代回收的任務(wù),效率可想而知。最后一個(gè)缺點(diǎn)就是標(biāo)記-清除算法帶來(lái)的內(nèi)存碎片問(wèn)題,這個(gè)問(wèn)題可以通過(guò)參數(shù)-XX:+UseCMSCompactAtFullCollection來(lái)緩解(默認(rèn)開(kāi)啟),用于在CMS進(jìn)行Full GC的時(shí)候進(jìn)行碎片的合并整理,但是內(nèi)存整理并不是并發(fā)的,會(huì)造成的應(yīng)用程序的停頓,所以通常配合-XX:CMSFullGCsBeforeCompaction參數(shù)一起使用,該參數(shù)表示在允許連續(xù)幾次的不整理碎片的CMS GC。
G1收集器
Garbage First 收集器應(yīng)該是當(dāng)今最前沿的垃圾收集器了,它的優(yōu)點(diǎn)是:
并行與并發(fā):G1能充分利用多CPU、多核環(huán)境下的硬件優(yōu)勢(shì),使用多個(gè)CPU(CPU或者CPU核心)來(lái)縮短Stop-The-World停頓的時(shí)間,部分其他收集器原本需要停頓Java線程執(zhí)行的GC動(dòng)作,G1收集器仍然可以通過(guò)并發(fā)的方式讓Java程序繼續(xù)執(zhí)行。
分代收集:與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能獨(dú)立管理整個(gè)GC堆,但它能夠采用不同的方式去處理新創(chuàng)建的對(duì)象和已經(jīng)存活了一段時(shí)間、熬過(guò)多次GC的舊對(duì)象以獲取更好的收集效果。
空間整合:與CMS的“標(biāo)記—清理”算法不同,G1從整體來(lái)看是基于“標(biāo)記—整理”算法實(shí)現(xiàn)的收集器,從局部(兩個(gè)Region之間)上來(lái)看是基于“復(fù)制”算法實(shí)現(xiàn)的,但無(wú)論如何,這兩種算法都意味著G1運(yùn)作期間不會(huì)產(chǎn)生內(nèi)存空間碎片,收集后能提供規(guī)整的可用內(nèi)存。這種特性有利于程序長(zhǎng)時(shí)間運(yùn)行,分配大對(duì)象時(shí)不會(huì)因?yàn)闊o(wú)法找到連續(xù)內(nèi)存空間而提前觸發(fā)下一次GC。
可預(yù)測(cè)的停頓:這是G1相對(duì)于CMS的另一大優(yōu)勢(shì),降低停頓時(shí)間是G1和CMS共同的關(guān)注點(diǎn),但G1除了追求低停頓外,還能建立可預(yù)測(cè)的停頓時(shí)間模型,能讓使用者明確指定在一個(gè)長(zhǎng)度為M毫秒的時(shí)間片段內(nèi),消耗在垃圾收集上的時(shí)間不得超過(guò)N毫秒,這幾乎已經(jīng)是實(shí)時(shí)Java(RTSJ)的垃圾收集器的特征了。
摘錄來(lái)自: 周志明. “深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐(第2版)”。 iBooks.
G1 收集器雖然還有老年代和新生代的感念,但是內(nèi)存布局上卻沒(méi)有為其劃定單獨(dú)的物理隔離的區(qū)域,而是分給它們不同數(shù)量(無(wú)需連續(xù))的Region。G1 能夠預(yù)測(cè)停頓的功能就是依賴于 Region 這個(gè)東西實(shí)現(xiàn)的,因?yàn)閷?nèi)存分割成多個(gè)大小相等的區(qū)域,然后在后臺(tái)維護(hù)一個(gè)垃圾回收價(jià)值列表,每次根據(jù)允許的回收時(shí)間(使用參數(shù)-XX:MaxGCPauseMillis設(shè)定,默認(rèn)值為200)來(lái)確定那些 Region 進(jìn)行回收。理解起來(lái)很簡(jiǎn)單的 Region 回收,實(shí)現(xiàn)起來(lái)卻很困難,其中一個(gè)主要的原因就是 Region 之間的對(duì)象相互引用,如果到回收時(shí)才進(jìn)行可達(dá)性分析要掃描整個(gè)Java堆才能完成分析,同樣的問(wèn)題也存在于其他分代收集器新生代和老年代相互引用的關(guān)系中。虛擬機(jī)使用Remembered Set來(lái)避免掃描全堆,程序在對(duì) Reference 類(lèi)型進(jìn)行寫(xiě)操作的時(shí)候會(huì)檢測(cè)引用的對(duì)象是否存在于不同的 Region(或者是不同年代)中,如果是就將應(yīng)用信息記錄到 被引用對(duì)象 的 Remembered Set 中,在內(nèi)存回收時(shí),將 Remembered Set 加入 GC Roots 即可避免全堆掃描。
G1 收集器的運(yùn)作大致可以分為以下幾個(gè)步驟:
- 初始標(biāo)記(Initial Marking):僅僅標(biāo)記 GC Roots直接關(guān)聯(lián)的對(duì)象
- 并發(fā)標(biāo)記(Concurrent Marking):從 GC Roots開(kāi)始做可達(dá)性分析
- 最終標(biāo)記(Final Marking):修正在并發(fā)標(biāo)記階段變動(dòng)的標(biāo)記
- 篩選回收 (Live Data Counting and Evacuation):根據(jù)回收機(jī)制和成本排序,根據(jù)用戶期望的停頓時(shí)間制定回收計(jì)劃
線程運(yùn)行情況如下圖:
[圖片上傳失敗...(image-b1baff-1516334222313)]
更多可參考Oracle關(guān)于G1調(diào)優(yōu)的官方文檔:Garbage First Garbage Collector Tuning