JVM之垃圾回收機制(GC)
JVM之垃圾回收機制全解(GC)文章底部有思維導(dǎo)圖,較為清晰,可參考
導(dǎo)讀:垃圾回收是Java體系中最重要的組成部分之一,其提供了一套全自動的內(nèi)存管理方案,要想掌握這套管理方案,就必須了解垃圾回收器的工作原理。本文介紹了垃圾回收的概念,算法,垃圾回收器及我在工作中遇到的一些關(guān)于GC的優(yōu)化實例。
先來簡單了解下JVM:

-------------------------------------------------------------
一、heap內(nèi)存劃分
-------------------------------------------------------------
1.年輕代:分三個區(qū)。一個Eden區(qū),兩個Survivor區(qū)(from Survivor(s0)區(qū)和to Survivor(s1)區(qū))。
大部分對象在Eden區(qū)中生成。當(dāng)Eden區(qū)滿時,還存活的對象將被復(fù)制到Survivor區(qū)(兩個中的一個),當(dāng)這個Survivor區(qū)滿時,此區(qū)的存活對象將被復(fù)制到另外一個Survivor區(qū),當(dāng)這個Survivor去也滿了的時候,從第一個Survivor區(qū)復(fù)制過來的并且此時還存活的對象,將被復(fù)制“年老區(qū)(Tenured)”。
2.年老代
在年輕代中經(jīng)歷了N次((ParNew默認15))垃圾回收后仍然存活的對象,就會被放到年老代中。年輕代放不下的大對象直接進入老年代。
tip1:對象動態(tài)年齡計算規(guī)則
虛擬機并不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold(默認15次)才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
3.持久代
用于存放靜態(tài)文件,如今Java類、方法等
JDK1.8中,永久代已經(jīng)從java堆中移除,String直接存放在堆中,類的元數(shù)據(jù)存儲在meta space中,meta space占用外部內(nèi)存,不占用堆內(nèi)存。

-------------------------------------------------------------
二、GC回收算法
-------------------------------------------------------------
2.1 標記清除算法:標記清除分為兩個階段,標記階段(標記從根節(jié)點開始的所有可達對象,未標記即未被引用)和清除階段。缺點:兩個階段效率都很低;回收后內(nèi)存空間不連續(xù),產(chǎn)生碎片多,易導(dǎo)致提前GC。
2.2 復(fù)制算法:內(nèi)存等分兩塊,相互復(fù)制存活的對象后清洗垃圾 缺點:內(nèi)存利用率低
2.3 標記壓縮法:先標記,然后存活的向一段移動,清理存活端標記以外的內(nèi)存。(老年代使用,無需需要第二塊相同的內(nèi)存) 優(yōu)缺點:無內(nèi)存碎片,但是耗時。
2.4 分代算法:復(fù)制算法(新生代使用) ,標記壓縮法和標記清除法(老年代使用)。卡表(數(shù)據(jù)結(jié)構(gòu),一個比特位的集合),用來表示老年代對象是否持有新生代對象的引用,新生代無需再花時間確認對象是否被持有,可以加快新生代回收的速度。
2.5 分區(qū)算法:將整個堆空間劃分為連續(xù)不同的小的空間,獨立管理,獨立回收。
垃圾回收基本思想在于如何判斷對象的可觸及性。根據(jù)標記清除算法,可以掃描出root節(jié)點未觸及持有的對象,但一個無法觸及持有的對象有可能在某個時間下使自己復(fù)活。
對象的可觸及性的三種狀態(tài):
可觸及的
可復(fù)活的(finalize()函數(shù))
不可觸及的(finalize()函數(shù)只能調(diào)用一次)
2.6 引用和可觸及的強度分為4個級別
強引用:任何時候都不會被系統(tǒng)回收,亦可能會引起OOM。 例:StringBuffer str = new StringBuffer("juejin");
軟引用:GC不一定回收,但堆空間不足時會被回收。OOM之前一定會回收,所以軟引用不會引起OOM。 使用SoftReference創(chuàng)建的對象。
弱引用:發(fā)現(xiàn)即回收。使用WeakReference創(chuàng)建的對象。使用PhantomReference創(chuàng)建的對象。
虛引用:隨時可回收。
-------------------------------------------------------------
三、分代垃圾回收
-------------------------------------------------------------
3.1 young代采用復(fù)制算法
3.2 old代使用標記清除或者標記清理
3.3 Tip:對象優(yōu)先在Eden去分配,大的對象直接進入老年代,長期存活對象進入老年代。
-------------------------------------------------------------
四、垃圾回收器
-------------------------------------------------------------
tip:Stop The World(STW)1、為了讓垃圾回收器可以正常切高效執(zhí)行。2、保證了系統(tǒng)某個瞬間的一致性。3、有益于垃圾回收器更好地標記垃圾對象。
4.1 串行回收器
單線程GC,啟動時會停止應(yīng)用,適用于配置小的服務(wù)器(1C2G),基本已棄用
4.2 并行回收器PS(吞吐量優(yōu)先)
JDK1.6~1.8默認使用。垃圾線程并行,啟動時應(yīng)用會等待。
PS的新生代回收器有兩個。
(1)、ParNew回收器:多線程執(zhí)行垃圾回收。PS的線程數(shù)量可以用-XX:ParallelGCThreads指定。當(dāng)CPU<8時,ParallelGCThreads的值=CPU,CPU>8時,ParallelGCThreads的值=3+((5*CPU_count)/8)。適用于交互較弱的場景。JDK1.8以上已經(jīng)被刪除。
(2)、Parallel回收器:與ParNew一樣是多線程獨占式。但其特點是關(guān)注系統(tǒng)的吞吐量(吞吐量:花費在垃圾收集時間和花費在應(yīng)用時間的占比)。
使用方法:-XX:+UseParallelGC(設(shè)置老年代-XX:+UseParallelOidGC)
4.3 并發(fā)回收器(響應(yīng)時間優(yōu)先)(并行GC前會額外觸發(fā)新生代的GC)
與并行回收器不相同的是,并發(fā)收集器是非獨占式,在進行垃圾回收的時候應(yīng)用程序也可以運行。
主要有Concurrent Mask Sweep(CMS)和G1
JDK1.9默認使用G1。適用于對響應(yīng)時間有要求的場景。響應(yīng)時間:花費在應(yīng)用時間和花費在垃圾收集時間的占比。
CMS(以獲取最短回收停頓時間為目標的收集器,基于并發(fā)“標記清理”實現(xiàn))
過程:1)初始標記(標記root對象) 2)并發(fā)標記 3)預(yù)清理(準備及控制停頓時間) 4)重新標記 5)并發(fā)清除 6)并發(fā)重置
優(yōu)點:并發(fā)收集、低停頓。
缺點:
1)CMS對CPU資源敏感。在并發(fā)階段,它雖然不會導(dǎo)致用戶線程停頓,但是會因為占用了一部分線程而導(dǎo)致應(yīng)用程序變慢,總吞吐量會降低。
2)CMS無法處理浮動垃圾,可能會出現(xiàn)“Concurrent Mode Failure(并發(fā)模式故障)”失敗而導(dǎo)致Full GC產(chǎn)生。
3)CMS容易出現(xiàn)大量空間碎片。當(dāng)空間碎片過多,將會給大對象分配帶來很大的麻煩,往往會出現(xiàn)老年代還有很大空間剩余,但是無法找到足夠大的連續(xù)空間來分配當(dāng)前對象,不得不提前觸發(fā)一次Full GC。
4)老年代垃圾回收過程中,如果出現(xiàn)資源不夠用,則會強制進行老年代串行回收,應(yīng)用暫停時間更長,影響更大。

G1(面向服務(wù)端應(yīng)用的垃圾收集器)
1.7正式使用,且使用了全新的算法,看起來有取代CMS的趨勢。
保留了分代的概念,但是從堆結(jié)構(gòu)上看,分代內(nèi)存并不是連續(xù)的。如圖:
在并行性和并發(fā)性的基礎(chǔ)上,可以同時兼顧年輕代和年老代,還可以進行空間整理,每次GC之后會自動進行碎片整理,減少碎片空間。最后還有可預(yù)見性,G1可以選取部分區(qū)域進行內(nèi)存回收。
過程:1)初始標記(標記root對象)(eden區(qū)會被清空) 2)根區(qū)域掃描 3)并發(fā)標記 4)重新標記 5)獨占清理 (計算各個區(qū)域存活對象和GC回收比例)6)并發(fā)清理
混合回收:在年輕代滿時,觸發(fā)年輕代收集;隨著老年代內(nèi)存增長,當(dāng)?shù)竭_IHOP閾值-XX:InitiatingHeapOccupancyPercent(老年代占整堆比,默認45%)時,G1開始準備收集老年代空間。首先經(jīng)歷并發(fā)標記周期,識別出垃圾占比較高的老年代分區(qū)。但隨后G1并不會馬上開始一次混合收集,而是讓應(yīng)用線程先運行一段時間,等待觸發(fā)一次年輕代收集。在這次STW中,G1將保準整理混合收集周期。接著再次讓應(yīng)用線程運行,當(dāng)接下來的幾次年輕代收集時,將會有老年代分區(qū)加入到CSet中,即觸發(fā)混合收集,這些連續(xù)多次的混合收集稱為混合收集周期(Mixed Collection Cycle)
特點:
1、并行于并發(fā):G1能充分利用CPU多核,使用多個CPU來縮短stop-The-World停頓時間。
2、分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。
3、空間整合:與CMS的“標記--清理”算法不同,G1從整體來看是基于“標記整理”算法實現(xiàn)的收集器;從局部上來看是基于“復(fù)制”算法實現(xiàn)的。
4、可預(yù)測的停頓:這是G1相對于CMS的另一個大優(yōu)勢,降低停頓時間是G1和CMS共同的關(guān)注點,但G1除了追求低停頓外,還能建立可預(yù)測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內(nèi)。

-------------------------------------------------------------
五、調(diào)優(yōu)思路
-------------------------------------------------------------
5.1 前瞻
1、嘗試多種垃圾回收器,G1并不是最好的
2、并發(fā)不等于并行。垃圾回收的過程實際上有兩步,啟動GC周期和GC自身運行,這是不同的兩件事。并發(fā)針對的是GC周期,而并行針對GC算法自身。
3、平均事務(wù)時間不是最需要被關(guān)注的指標,有可能用戶正好經(jīng)歷了那個長時間GC的場景,那將是毀滅性的。
4、GC調(diào)優(yōu)并不能解決所有的事。如果程序修改程度大,那應(yīng)該優(yōu)先優(yōu)化架構(gòu)及代碼。
5、GC日志并不會對性能造成太大的影響,在GC未被優(yōu)化之前,開啟GC日志是有必要的。
6、降低新對象的分配率可以改善GC的運行狀況。粗略地把系統(tǒng)中的對象分為三種:長命(long-lived)對象,對它們我們一般做不了什么;中等壽命(mid-lived)對象,最大的問題可能出現(xiàn)在這;短命(short-lived)對象,它們的釋放和回收通常都很快,在下個GC周期來臨時就會消失
5.2 思路
1、理解應(yīng)用需求和問題。
2、掌握GC的狀態(tài)。
3、思考選擇的GC是否符合我們的應(yīng)用特征。
4、分析確認需要調(diào)整的參數(shù)。
5、驗證調(diào)優(yōu)。
5.3 查看設(shè)置參數(shù)
java -XX:+PrintFlagsInitial <pid>查看初始值
-XX:+PrintFlagsFinal 查看最終值(初始值可能被修改掉)
-Xms 默認情況下堆內(nèi)存的64分之一
-Xmx 默認情況下對內(nèi)存的4分之一
-Xmn 默認情況下堆內(nèi)存的3分之一
-XX:NewRatio 默認為2
-XX:SurvivorRatio 默認為8
-XX:+PrintGCDetails 開啟GC詳細日志-Xloggc:/cpic/cpicapp/perfma/xowl/../logs/xowl/gc.log -XX:+PrintGCDetails
5.4 GC一般合理表現(xiàn)
分析結(jié)果顯示GC耗時在0.1-0.3秒以內(nèi)的話,一般不需要花費額外的時間做GC調(diào)優(yōu)。然而, 如果GC耗時達到1-3秒甚至10秒以上,就需要立即對系統(tǒng)進行GC調(diào)優(yōu) 。
Minor GC執(zhí)行迅速(50毫秒以內(nèi))
Minor GC執(zhí)行不頻繁(間隔10秒左右一次)
Full GC執(zhí)行迅速(1秒以內(nèi))
Full GC執(zhí)行不頻繁(間隔10分鐘左右一次)
-------------------------------------------------------------
六、參數(shù)調(diào)優(yōu)
-------------------------------------------------------------
6.1 PS
6.1.1 吞吐量:-XX:GCTimeRatio=<N>垃圾收集時間與應(yīng)用程序時間的比率設(shè)置為1/(1+<n>),默認值是99%(垃圾收集時間的1%)。
-Xmx<N>:指定最大堆占用空間。
優(yōu)先級保證:暫停時間>吞吐量>堆空間。如果不設(shè)置初始堆內(nèi)存和最大堆內(nèi)存,則初始堆大小為物理內(nèi)存的1/64,最大內(nèi)存為1/4,年輕代大小為堆內(nèi)存的1/3。
-Xms (初始堆內(nèi)存) and -Xmx (最大堆內(nèi)存):
如果知道應(yīng)用程序需要多少堆才能正常工作,那么可以將-Xms和-Xmx設(shè)置為相同的值。如果不知道,那么JVM將首先使用初始堆大小,然后自動增長,直到它找到堆使用和性能之間的平衡。
三個重要參數(shù):1、-XX:MaxGCPauseTimeMillis:設(shè)置最大垃圾回收時間停頓時間。
2、-XX:GCTimeRatio:設(shè)置吞吐量大小。
3、-XX:UseAdaptiveSizePolicy:自適應(yīng)模式。新生代大小,eden區(qū)與survivor區(qū)的比例,晉升老年代的對象年齡等參數(shù)會被自動調(diào)整。
CMS
使用方法:-XX:+UseConcMarkSweepGC
并發(fā)線程數(shù):(ParallelGCThreads+3)/4。也可用通過-XX:ConcGCThreads或者-XX:ParallelCMSThreads手工設(shè)置。
因為并發(fā)性質(zhì),所以CMS不會等到堆飽和時才進行垃圾回收。默認值為老年代占用率68%,通過-XX:CMSInitiatingOccupancyFraction設(shè)置。
內(nèi)存壓縮:設(shè)定多少次之后GC回收之后對內(nèi)存進行一次壓縮。-XX:CMSFullGCsBeforeCompaction。默認0
開啟-XX:CMSClassUnloadingEnable,可以在需要時候Perm區(qū)的還會觸發(fā)一次FullGC。
6.2 G1
6.2.1 啟用G1(常用):-XX:+UseG1GC
堆內(nèi)存(常用):-XX:InitialHeapSize(初始堆內(nèi)存)-XX:MaxHeapSize(最大堆內(nèi)存)
年輕代設(shè)置(常用):-XX:NewSize(最小) -XX:MaxNewSize(最大)
暫停時間(常用):-XX:MaxGCPauseTimeMillis=<N>(默認200ms)
空閑堆占比:-XX:MinHeapFreeRatio=40(GC后,如果發(fā)現(xiàn)空閑堆內(nèi)存占到整個預(yù)估堆內(nèi)存的40%,則放大堆內(nèi)存的預(yù)估最大值,但不超過固定最大值。)-XX:MaxHeapFreeRatio=70
最大暫停間隔時間:-XX:PauseTimeIntervalMillis
GC停頓時候的并行的GC收集線程數(shù):-XX:ParallelGCThreads=< ergo>根據(jù)虛擬機所在的主機的可用CPU線程數(shù)來計算的:如果CPU少于8個這個值就是cpu的數(shù)量,否則,就等于cpu數(shù)量*5/8。每個停頓開始的時候,最大的GC線程數(shù)還受限于最大的堆內(nèi)存,G1的內(nèi)個線程能使用的最大堆內(nèi)存是由-XX:HeapSizePerGCThread來設(shè)置的。
與應(yīng)用并發(fā)執(zhí)行的GC線程數(shù):-XX:ConcGCThreads=< ergo>:默認是-XX:ParallelGCThreads/4
region的大小:-XX:G1HeapRegionSize=< ergo>整個堆大概有2048個region,region的大小可以在1-32M之間,必須是2的次方。調(diào)整之后會影響分配對象的大小及停頓時間。
可分配的最大對象的大小: -XX:G1HeapRegionSize-XX:G1MaxNewSizePercent
-------------------------------------------------------------
七、針對項目經(jīng)驗集
-------------------------------------------------------------
7.1 這幾種GC收集器相比之下,只要JDK版本在1.7u4及以上,推薦使用G1收集器。JDK1.7,1.8都默認使用PS(并行收集器)
7.2 尤其注意容器項目,容器設(shè)置的JVM配置內(nèi)存大小不能大于容器內(nèi)存大小,否則參數(shù)配置無效。
7.3 調(diào)優(yōu)實例
7.3.1 實例一(集團2018版XXXXXXXX系統(tǒng)):
壓測表現(xiàn):穩(wěn)定性壓測時結(jié)果不穩(wěn)定且內(nèi)存消耗一直80%左右,影響時間3天。
內(nèi)存分析表現(xiàn):堆內(nèi)存很大(7G)但年輕代內(nèi)存非常小,年輕代minGC頻繁,老年代內(nèi)存一直增加直至觸發(fā)majorGC,且GC暫停時間長,平均200~300ms
調(diào)優(yōu):調(diào)整年輕代內(nèi)存為3G
export JAVA_OPTS="$JAVA_OPTS -Xmx7g -Xms7g -XX:NewSize=3g -XX:MaxNewSize=3g -XX:+UseG1GC"
優(yōu)化結(jié)果:
復(fù)壓,內(nèi)存穩(wěn)定在60%左右,年輕代GC頻繁度減小,GC耗時100ms左右,老年代穩(wěn)定無majorGC
7.3.2 實例二(壽險2013版XXXXXXXX系統(tǒng)):
壓測表現(xiàn):壓測時壓不上去,服務(wù)器消耗未滿載。且壓的時間長了TPS會有斷崖式下降,TPS和相應(yīng)時間非常不穩(wěn)定。影響時間2天。
內(nèi)存分析表現(xiàn):heap內(nèi)存只設(shè)置了2G,容器是3.5C7G,老年代一直增長直至觸發(fā)majorGC,GC頻繁且GC暫停時間不穩(wěn)定。
調(diào)優(yōu):調(diào)整heap內(nèi)存大小7G,新生代3G。
JAVA_OPTS="-Xmx7000m -Xms7000m -Xmn3072m -XX:PermSize=256M -XX:MaxPermSize=256M
調(diào)優(yōu)結(jié)果:
TPS增長40~50且穩(wěn)定。老年代穩(wěn)定無majorGC。minorGC頻繁度減小,GC暫停時間降低且穩(wěn)定在幾十ms以內(nèi)。
7.3.3 Tip1:年輕代內(nèi)存并不是越大越好,雖然會減小GC的頻率,但是在GC時會增加回收時間造成GC暫停時間長。
Tip2:docker系統(tǒng),如果容器內(nèi)存4G,堆內(nèi)存設(shè)置6G,進程可以啟動且顯示為6G,但實際只能使用到4G。
Tip3:如果開發(fā)和測試都不清楚如何設(shè)置堆大小及年輕代大小,可以參考perfma產(chǎn)品 http://xxfox.perfma.com/ ,填寫相關(guān)參數(shù)會給出調(diào)優(yōu)建議

浙公網(wǎng)安備 33010602011771號