jvm垃圾收集器介紹
新生代收集器(young generation)
1,serial收集器
是最基本的,發(fā)展最悠久的。是一個單線程收集器,只會使用一個cpu或者一個線程完成垃圾收集。它進行垃圾收集時,必須暫停其他所有的線程直到收集結(jié)束。serial收集器對于運行在client模式下的虛擬機來說是一個很好地選擇。使用標記復(fù)制方法來進行新生代的清理。使用serial收集器:-XX:+UseSerialGC(serial + serial old)
2,ParNew收集器
是serial收集器的多線程版本,除了使用多線程進行垃圾收集之外,其余的行為包括Serial收集器可用的所有控制參數(shù)(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、stop the world、對象分配規(guī)則、回收策略等都與serial收集器完全一樣,在實現(xiàn)上,兩者公用了相當(dāng)多的代碼。使用ParNew收集器: -XX+UseParNewGC(ParNew + Serial Old)
3,Parallel Scavenge收集器
Parallel Scavenge收集器是一個新生代收集器,它也是使用復(fù)制算法的收集器,又是并行的多線程收集器.。parallel Scavenge收集器的目標是達到一個可控制的吞吐量。所謂吞吐量就是CPU用于運行用戶代碼的時間與CPU總消耗時間的比值,吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。
使用parallel Scavenge收集器: -XX:+UseParallelGC(Parallel Scavenge + Serial Old)-XX:+UseParallelOldGC(Parallel Scavenge + Parallel Old)
Parallel Scavenge收集器提供了兩個參數(shù)用于精確控制吞吐量,分別是控制最大垃圾收集停頓時間-XX:MaxGCPauseMilli參數(shù)以及直接設(shè)置吞吐量大小的-XX:GCTimeRatio參數(shù):
MaxGCPauseMillis參數(shù)允許的值是一個大于0的毫秒數(shù),收集器將盡可能保證內(nèi)存回收花費的時間不超過設(shè)定值。
GCTimeRatio參數(shù)的值應(yīng)當(dāng)是一個大于0且小于100的整數(shù),-XX:GCTimeRatio=nnn:means not more than 1 / (1 + nnn) of the application execution time be spent in the collector。默認為99,就是允許最大1%(即1/(1+99))的垃圾收集時間。
由于與吞吐量關(guān)系密切,Parallel Scavenge收集器也經(jīng)常稱為"吞吐量優(yōu)先"收集器。除上述兩個參數(shù)之外,Parallel Scavenge收集器還有一個參數(shù)-XX:+UseAdaptiveSizePolicy值的關(guān)注。這是一個開關(guān)參數(shù),當(dāng)這個參數(shù)打開之后,就不需要手動指定新生代的大小(-Xmn)、Eden與Survivor區(qū)的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節(jié)參數(shù)了,虛擬機會根據(jù)當(dāng)前系統(tǒng)的運行情況收集性能監(jiān)控信息,動態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時間或者最大的吞吐量,這種調(diào)節(jié)方式稱為GC自適應(yīng)的調(diào)節(jié)策略(GC Ergonomics)。
使用Parallel Scavenge自適應(yīng)調(diào)節(jié)策略,把內(nèi)存管理的調(diào)優(yōu)交給虛擬機去完成是一個不錯的選擇。只需要把基本的內(nèi)存數(shù)據(jù)設(shè)置好(如 -Xmx設(shè)置最大堆),然后使用MaxGCPauseMillis參數(shù)(更關(guān)注最大停頓時間)或GCTimeRatio(更關(guān)注吞吐量)參數(shù)給虛擬機設(shè)立一個優(yōu)化目標,那具體細節(jié)參數(shù)的調(diào)節(jié)工作就由虛擬機完成了。自適應(yīng)調(diào)節(jié)策略也是Parallel Scavenge收集器與ParNew收集器的一個重要區(qū)別。
Serial,ParNew,Parallel scavenge這幾個新生代收集器都是使用標記復(fù)制方法進行垃圾收集
老年代收集器(old generation)
4,Serial Old收集器
這個收集器的主要意義也是在于給Client模式下的虛擬機使用。如果在Server模式下,那么它主要還有兩大用途:一種用途是在JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另一種用途就是作為CMS收集器的后備預(yù)案,在并發(fā)收集發(fā)生Concurrent Mode Failure時使用。這兩點都將在后面的內(nèi)容中詳細講解。使用標記清理整理方法來進行垃圾清理。
5,Parallel old收集器
是Parallel Scavenge收集器的老年代版本,使用多線程和"標記-清理-整理"算法。在注重吞吐量以及CPU資源敏感的場合,都可以優(yōu)先考慮Parallel Scavenge加Parallel Old收集器。Parallel Old收集器的工作過程如圖3-9所示。
6,cms收集器 Concurrent Mark Sweep
是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應(yīng)用集中在互聯(lián)網(wǎng)或者B/S系統(tǒng)的服務(wù)端上,這類應(yīng)用尤其重視服務(wù)的響應(yīng)速度,希望系統(tǒng)停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應(yīng)用的需求。
從名字("Mark Sweep")上看,CMS收集器是基于"標記-清除"算法實現(xiàn)的,它的運作過程相對于前面幾種收集器來說更復(fù)雜一些,整個過程分為4個步驟,包括:
(1)初始標記(CMS initial mark),記錄gc roots直接能引用到的對象,速度很快。
(2)并發(fā)標記(CMS concurrent mark),并發(fā)標記階段就是進行GC Roots Tracing的過程;
(3)重新標記(CMS remark),重新標記階段則是為了修正并發(fā)標記期間因用戶程序繼續(xù)運作而導(dǎo)致標記產(chǎn)生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比并發(fā)標記的時間短。
(4)并發(fā)清除(CMS concurrent sweep)
其中,初始標記、重新標記這兩個步驟仍然需要"Stop The World"。初始標記僅僅只是標記一下GC Roots能直接關(guān)聯(lián)到的對象,速度很快;耗時最大的并發(fā)標記和并發(fā)清除過程收集器線程都可以與用戶線程一起工作。總體上來說,CMS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)執(zhí)行的。
主要優(yōu)點:
- 并發(fā)收集、低停頓。
幾個明顯的缺點:
- 對CPU資源敏感(會和服務(wù)搶資源);
- 無法處理浮動垃圾(在并發(fā)清理階段又產(chǎn)生垃圾,這種浮動垃圾只能等到下一次gc再清理了);
- 它使用的回收算法-“標記-清除”算法會導(dǎo)致收集結(jié)束時會有大量空間碎片產(chǎn)生,當(dāng)然通過參數(shù)-XX:+UseCMSCompactAtFullCollection 可以讓jvm在執(zhí)行完標記清除后再做整理
- 執(zhí)行過程中的不確定性,會存在上一次垃圾回收還沒執(zhí)行完,然后垃圾回收又被觸發(fā)的情況,特別是在并發(fā)標記和并發(fā)清理階段會出現(xiàn),一邊回收,系統(tǒng)一邊運行,也許沒回收完就再次觸發(fā)full gc,也就是"concurrent mode failure",此時會進入stop the world,用serial old垃圾收集器來回收
parallel Scavenge與cms不能配合工作,老年代使用cms垃圾收集器,那么新生代只能選擇ParNew或者Serial收集器中的一個。
ParNew是使用-XX:+UseConcMarkSweepGC選項后的默認新生代收集器,也可以使用-XX:+UseParNewGC選項來強制指定它。
CMS的相關(guān)參數(shù)
- -XX:+UseConcMarkSweepGC:啟用cms
- -XX:ConcGCThreads:并發(fā)的GC線程數(shù)
- -XX:+UseCMSCompactAtFullCollection:FullGC之后做壓縮整理(減少碎片)
- -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后壓縮一次,默認是0,代表每次
FullGC后都會壓縮一次 - -XX:CMSInitiatingOccupancyFraction: 當(dāng)老年代使用達到該比例時會觸發(fā)FullGC(默認
是92,這是百分比) - -XX:+UseCMSInitiatingOccupancyOnly:只使用設(shè)定的回收閾值(-
XX:CMSInitiatingOccupancyFraction設(shè)定的值),如果不指定,JVM僅在第一次使用設(shè)定
值,后續(xù)則會自動調(diào)整 - -XX:+CMSScavengeBeforeRemark:在CMS GC前啟動一次minor gc,目的在于減少
老年代對年輕代的引用,降低CMS GC的標記階段時的開銷,一般CMS的GC耗時 80%都在
remark階段
7,G1收集器
G1 (Garbage-First)是一款面向服務(wù)器的垃圾收集器,主要針對配備多顆處理器及大容量內(nèi)存的機器. 以極高概率滿足GC停頓時間要求的同時,還具備高吞吐量性能特征.
G1將Java堆劃分為多個大小相等的獨立區(qū)域(Region),JVM最多可以有2048個Region。一般Region大小等于堆大小除以2048,比如堆大小為4096M,則Region大小為2M,當(dāng)然也可以用參數(shù)"-XX:G1HeapRegionSize"手動指定Region大小,但是推薦默認的計算方式。G1保留了年輕代和老年代的概念,但不再是物理隔閡了,它們都是(可以不連續(xù))Region的集合。
默認年輕代對堆內(nèi)存的占比是5%,如果堆大小為4096M,那么年輕代占據(jù)200MB左右的內(nèi)存,對應(yīng)大概是100個Region,可以通過“-XX:G1NewSizePercent”設(shè)置新生代初始占比,在系統(tǒng)運行中,JVM會不停的給年輕代增加更多的Region,但是最多新生代的占比不會超過60%,可以通過“-XX:G1MaxNewSizePercent”調(diào)整。年輕代中的Eden和Survivor對應(yīng)的region也跟之前一樣,默認8:1:1,假設(shè)年輕代現(xiàn)在有1000個region,eden區(qū)對應(yīng)800個,s0對應(yīng)100個,s1對應(yīng)100個。
一個Region可能之前是年輕代,如果Region進行了垃圾回收,之后可能又會變成老年代,也就是說Region的區(qū)域功能可能會動態(tài)變化。
G1垃圾收集器對于對象什么時候會轉(zhuǎn)移到老年代跟之前講過的原則一樣,唯一不同的是對大對象的處理,G1有專門分配大對象的Region叫Humongous區(qū),而不是讓大對象直接進入老年代的Region中。在G1中,大對象的判定規(guī)則就是一個大對象超過了一個Region大小的50%,比如按照上面算的,每個Region是2M,只要一個大對象超過了1M,就會被放入Humongous中,而且一個大對象如果太大,可能會橫跨多個Region來存放。
Humongous區(qū)專門存放短期巨型對象,不用直接進老年代,可以節(jié)約老年代的空間,避免因為老年代空間不夠的GC開銷。
Full GC的時候除了收集年輕代和老年代之外,也會將Humongous區(qū)一并回收。
G1收集器一次GC的運作過程大致分為以下幾個步驟:
- 初始標記(initial mark,STW):暫停所有的其他線程,并記錄下gc roots直接能引用的對象,速度很快 ;
- 并發(fā)標記(Concurrent Marking):同CMS的并發(fā)標記
- 最終標記(Remark,STW):同CMS的重新標記
-
篩選回收(Cleanup,STW):篩選回收階段首先對各個Region的回收價值和成本進行排序,根據(jù)用戶所期望的GC停頓時間(可以用JVM參數(shù) -XX:MaxGCPauseMillis指定)來制定回收計劃,比如說老年代此時有1000個Region都滿了,但是因為根據(jù)預(yù)期停頓時間,本次垃圾回收可能只能停頓200毫秒,那么通過之前回收成本計算得知,可能回收其中800個Region剛好需要200ms,那么就只會回收800個Region,盡量把GC導(dǎo)致的停頓時間控制在我們指定的范圍內(nèi)。這個階段其實也可以做到與用戶程序一起并發(fā)執(zhí)行,但是因為只回收一部分Region,時間是用戶可控制的,而且停頓用戶線程將大幅提高收集效率。不管是年輕代或是老年代,回收算法主要用的是復(fù)制算法,將一個region中的存活對象復(fù)制到另一個region中,這種不會像CMS那樣回收完因為有很多內(nèi)存碎片還需要整理一次,G1采用復(fù)制算法回收幾乎不會有太多內(nèi)存碎片。
image.png
G1收集器在后臺維護了一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先選擇回收價值最大的Region(這也就是它的名字Garbage-First的由來),比如一個Region花200ms能回收10M垃圾,另外一個Region花50ms能回收20M垃圾,在回收時間有限情況下,G1當(dāng)然會優(yōu)先選擇后面這個Region回收。這種使用Region劃分內(nèi)存空間以及有優(yōu)先級的區(qū)域回收方式,保證了G1收集器在有限時間內(nèi)可以盡可能高的收集效率。
具備以下特點:
- 并行與并發(fā):G1能充分利用多CPU、多核環(huán)境下的硬件優(yōu)勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓的時間,G1收集器仍然可以通過并發(fā)的方式讓java程序繼續(xù)執(zhí)行。
- 分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠采用不同的方式去處理新創(chuàng)建的對象和已經(jīng)存活了一段時間、熬過多次GC的舊對象以獲取更好的收集效果。
- 空間整合:與CMS的"標記-清理"算法不同,G1從整體上來看是基于"標記-整理"算法實現(xiàn)的收集器,從局部上看是基于"復(fù)制"算法實現(xiàn)的,但無論如何,這幾個算法都意味著G1運行期間不會產(chǎn)生內(nèi)存碎片,收集后能提供規(guī)整的可用內(nèi)存。
- 可預(yù)測的停頓:這是G1相對于CMS的另一大優(yōu)勢,降低停頓時間是G1和CMS共同的關(guān)注點。G1除了追求低停頓外,還能建立可預(yù)測的停頓啥時間模型,可以明確指定在一個長度為M毫秒的時間片段內(nèi),消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經(jīng)是實時Java(RTSJ)的垃圾收集器的特征了。
G1垃圾收集分類
YoungGC
YoungGC并不是說現(xiàn)有的Eden區(qū)放滿了就會馬上觸發(fā),而且G1會計算下現(xiàn)在Eden區(qū)回收大概要多久時間,如果回收時間遠遠小于參數(shù) -XX:MaxGCPauseMills 設(shè)定的值,那么增加年輕代的region,繼續(xù)給新對象存放,不會馬上做Young GC,直到下一次Eden區(qū)放滿,G1計算回收時間接近參數(shù) -XX:MaxGCPauseMills 設(shè)定的值,那么就會觸發(fā)Young GC
MixedGC
不是FullGC,老年代的堆占有率達到參數(shù)(-XX:InitiatingHeapOccupancyPercen)設(shè)定的值則觸發(fā),回收所有的Young和部分Old(根據(jù)期望的GC停頓時間確定old區(qū)垃圾收集的優(yōu)先順序)以及大對象區(qū),正常情況G1的垃圾收集是先做MixedGC,主要使用復(fù)制算法,需要把各個region中存活的對象拷貝到別的region里去,拷貝過程中如果發(fā)現(xiàn)沒有足夠的空region能夠承載拷貝對象就會觸發(fā)一次Full GC
Full GC
停止系統(tǒng)程序,然后采用單線程進行標記、清理和壓縮整理,好空閑出來一批Region來供下一次MixedGC使用,這個過程是非常耗時的。
G1垃圾收集器優(yōu)化建議
假設(shè)參數(shù) -XX:MaxGCPauseMills 設(shè)置的值很大,導(dǎo)致系統(tǒng)運行很久,年輕代可能都占用了堆內(nèi)存的60%了,此時才觸發(fā)年輕代gc。那么存活下來的對象可能就會很多,此時就會導(dǎo)致Survivor區(qū)域放不下那么多的對象,就會進入老年代中。
或者是你年輕代gc過后,存活下來的對象過多,導(dǎo)致進入Survivor區(qū)域后觸發(fā)了動態(tài)年齡判定規(guī)則,達到了Survivor區(qū)域的50%,也會快速導(dǎo)致一些對象進入老年代中。
所以這里核心還是在于調(diào)節(jié) -XX:MaxGCPauseMills 這個參數(shù)的值,在保證他的年輕代gc別太頻繁的同時,還得考慮每次gc過后的存活對象有多少,避免存活對象太多快速進入老年代,頻繁觸發(fā)mixed gc.
2 理解GC日志
33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]
100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
最前面的數(shù)字"33.125"和"100.667" 代表了GC發(fā)生的時間,這個數(shù)字的含義是從Java虛擬機啟動以來經(jīng)歷的秒數(shù)。
"[GC"和"[Full gc"說明了這次垃圾收集的停頓類型,書中說:而不是用來區(qū)分新生代GC還會老年代GC的。如果有"full",說明這次GC是發(fā)生了Stop-The-World的。我理解新生代和老年代GC都會發(fā)生Stop-The-World,只是老年代的STW的時間相對更長 GC說明垃圾回收發(fā)生在新生代,F(xiàn)ull GC說明垃圾回收發(fā)生在新生代和老年代
[DefNew、[Tenured、[Perm 表示GC發(fā)生的區(qū)域,這里顯示的區(qū)域名稱與使用的GC收集器是密切相關(guān)的。serial收集器的新生代名稱為"Default New Generation",所以顯示"[DefNew" 。如果是ParNew收集器,新生代名稱就會變成"[ParNew"意為"Parallel New Generation"。如果采用Parallel Scavenge收集器,那么它配套的新生代稱為"PSYoungGen",老年代和永久代同理,名稱也是由收集器決定的。
后面的方括號內(nèi)部的"3324K-> 152K(3712K)" 含義是"GC前該內(nèi)存區(qū)域已使用容量-> GC后該內(nèi)存區(qū)域已使用容量(該內(nèi)存區(qū)域總?cè)萘浚?, 3324K->152K(11904K)含義是"GC前整個堆的占用量-> GC后整個堆的占用量(堆的總大小)"
再往后,"0.0025925secs"表示該內(nèi)存區(qū)域GC所占用的時間,單位是秒。
參考
gc日志解析:https://plumbr.io/handbook/garbage-collection-algorithms-implementations
內(nèi)存分配和回收策略
對象優(yōu)先在eden分配
public class TestAllocation {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
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]; // 出現(xiàn)一次Minor GC
}
}
運行結(jié)果:
這次GC發(fā)生的原因是給allocation4分配內(nèi)存時,發(fā)現(xiàn)Eden已經(jīng)被占用了6MB,剩余空間已不足以分配案例location4所需的4MB的內(nèi)存,因此發(fā)生minor FC。GC期間虛擬機又發(fā)現(xiàn)已有3個2MB大小的對象全部無法放入到Survivor空間(Survivor只有1MB大小),所以只好通過分配擔(dān)保機制提前轉(zhuǎn)移到老年代去。
新生代GC(Minor GC): 指發(fā)生在新生代的垃圾收集動作,因為java對象大多都具備朝生夕死的特性,所以Minor GC非常頻繁,一般回收速度也比較快。
老年代GC(Major GC /Full GC):指發(fā)生在老年代的GC,出現(xiàn)了Major GC,經(jīng)常會伴隨至少一次的Minor GC,Major GC的速度一般比Minor GC慢10倍以上。
大對象直接進入老年代
-XX:PretenureSizeThreshold參數(shù),令大于這個設(shè)置值的對象直接在老年代分配。這樣做目的是避免在Eden區(qū)以及兩個Survivor區(qū)之間發(fā)生大量的內(nèi)存復(fù)制(新生代使用復(fù)制算法收集內(nèi)存)
public class TestAllocation1 {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation;
allocation = new byte[4* _1MB];
}
}
運行結(jié)果:
老年代的10MB空間被使用了40%,也就是4MB的allocation對象直接分配在老年代中,這是因為PretenureSizeThreshold被設(shè)置為3MB,因此超過3MB的對象都會直接在老年代進行分配。(PretenureSizeThreshold參數(shù)只對Serial和ParNew兩款收集器有效,Parallel Scavenge收集器不認識這個參數(shù))
長期存活對象進入老年代
public class TestTenuringThreshold {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3;
allocation1 = new byte[ _1MB/16];
allocation2 = new byte[4 * _1MB];
allocation3 = new byte[4 * _1MB]; // 第一次Minor GC
allocation3 = null;
allocation3 = new byte[4 * _1MB]; // 第二次Minor GC
}
}
~
當(dāng)MaxTenuringThreshold=1時,allocation3=new byte[4*_1MB] 第一次賦值時,因為eden區(qū)總空間為8192K,剩余空間不足4096K,所以會觸發(fā)一次minor GC。觸發(fā)GC后allocation1進入from區(qū)域,年齡為1,allocation2空間的大于suvivor的空間1MB,所以直接分配到老年代,allocation3分配到eden區(qū)占用51%的eden區(qū)空間。
allocation3=new byte[4*_1MB]第二次賦值時,eden區(qū)總空間為8192K,空間被占用51% eden區(qū)空間不足,所以觸發(fā)第二次Minor GC。第二次的Minor GC觸發(fā)后,由于MaxTenuringThreshold=1所以age=1的allocation1被提升到老年代,surivior的from區(qū)空間被清空,allocation3對象由于沒有被GC root引用,所有allocation3原來的空間被回收,再次被分配給新的申明的allocation3對象。
MaxTenuringThreshold=15時,流程大部分與上面相同,不同的地方在于age=1的allocation1經(jīng)過第二次GC后還是停留在surivior的from區(qū),不進入tenured generation老年代。
動態(tài)對象年齡的判定
public class TestTenuringThreshold2 {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3,allocation4;
allocation1 = new byte[ _1MB/4];
allocation4 = new byte[ _1MB/4];
allocation2 = new byte[4 * _1MB];
allocation3 = new byte[4 * _1MB];// 出現(xiàn)一次Minor GC
allocation3 = null;
allocation3 = new byte[4 * _1MB]; // 出現(xiàn)一次Minor GC
}
}
如果在Surivor空間的相同年齡所有對象大小總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無需等到MaxTenuringThreshold中要求的年齡。
上面allocation1與allocation4的對象加起來超過512KB并且是同年的,滿足同年對象達到Survivor空間的一半規(guī)則。
空間擔(dān)保
JDK 6 Update 24之后的規(guī)則變?yōu)橹灰夏甏倪B續(xù)空間大于新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,否則進行Full GC。如果HandlePromotionFailure失敗,那就只好在失敗后重新發(fā)起一次Full GC。