-
前言
開設(shè)JVM系列以后,就一直打算寫寫GC這塊的事情咙轩《笮總體上來說這塊也算是比較偏理論申鱼,很多時(shí)候是看的時(shí)候理解了愤诱,一段時(shí)間以后又忘記了,所以打算不僅寫寫理論還會(huì)寫上一些調(diào)優(yōu)的相關(guān)例子捐友,這樣會(huì)加深印象淫半。
-
堆內(nèi)存區(qū)域劃分
我們由GC的角度來進(jìn)行堆內(nèi)存的區(qū)域劃分:新生代和老年代。而新生代又可以劃分為Eden區(qū)楚殿、SurvivorFrom區(qū)撮慨、SurvivorTo區(qū)竿痰。而整個(gè)區(qū)域的比例應(yīng)該是新生代比老年代為1:2脆粥,而Eden比SurvivorFrom比SurvivorTo為8:1:1。
說完堆內(nèi)存的區(qū)域劃分以后影涉,我們?cè)俳o出關(guān)于設(shè)置堆內(nèi)存的相關(guān)參數(shù)大小的VM命令:
VM堆的相關(guān)參數(shù) | 描述 |
---|---|
-Xms | 設(shè)置JVM啟動(dòng)時(shí)堆的初始化內(nèi)存大小 |
-Xmx | 設(shè)置JVM的堆最大可用內(nèi)存大小 |
-Xmn | 設(shè)置新生代的空間大小变隔,剩下的為老年代的空間大小(-Xmn 是將NewSize與MaxNewSize設(shè)為一致)同下面兩個(gè)參數(shù)-XX:NewSize=XXXm與-XX:MaxNewSize=XXXm |
-XX:PermGen | 設(shè)置永久代內(nèi)存的初始化大行非恪(1.8以后就沒有永久代了匣缘,用元數(shù)據(jù)空間代替) |
-XX:MaxPermGen | 設(shè)置永久代的最大值(1.8以后就沒有永久代了,用元數(shù)據(jù)空間代替) |
-XX:MetaspaceSize | 元數(shù)據(jù)空間初始化大小 |
-XX:MaxMetaspaceSize | 元數(shù)據(jù)空間最大 |
-XX:SurvivorRatio | 提供Eden區(qū)和survivor區(qū)的空間比例鲜棠。比如肌厨,如果年輕代的大小為10m并且VM參數(shù)是-XX:SurvivorRatio=2,那么將會(huì)保留5m內(nèi)存給Eden區(qū)和每個(gè)Survivor區(qū)分配2.5m內(nèi)存豁陆。默認(rèn)比例是8 |
-XX:NewRatio | 提供年老代和年輕代的比例大小柑爸。默認(rèn)值是2 |
-Xss | Stack(棧)內(nèi)存大小設(shè)置(每個(gè)線程都會(huì)產(chǎn)生一個(gè)棧。在相同物理內(nèi)存下盒音,減小這個(gè)值能生成更多的線程表鳍。如果這個(gè)值太小會(huì)影響方法調(diào)用的深度) |
-XX:MaxTenuringThreshold | 設(shè)置新生代代對(duì)象進(jìn)入老年代的年齡(設(shè)置垃圾最大年齡。如果設(shè)置為0的話祥诽,則新生代對(duì)象不經(jīng)過Survivor區(qū)譬圣,直接進(jìn)入老年代。對(duì)于老年代比較多的應(yīng)用雄坪,可以提高效率厘熟。如果將此值設(shè)置為一個(gè)較大值,則新生代對(duì)象會(huì)在Survivor區(qū)進(jìn)行多次復(fù)制维哈,這樣可以增加對(duì)象再新生代的存活時(shí)間绳姨,增加在新生代即被回收的概論) |
通過上面的參數(shù)命令,大家可以嘗試的進(jìn)行相關(guān)JVM的內(nèi)存設(shè)置笨农,然后根據(jù)不同的設(shè)置來模擬各種OOM的情況就缆。
-
對(duì)象在內(nèi)存中的分配
1.對(duì)象被創(chuàng)建以后,進(jìn)行內(nèi)存分配的流程
首先會(huì)嘗試是否能直接分配到棧上空間(這個(gè)跟JIT的逃逸分析有關(guān))谒亦,如果不能則再次嘗試能否分配到TLAB上(本地線程分配緩沖區(qū)竭宰,存在與Eden區(qū)域),如果不能則對(duì)對(duì)象進(jìn)行大小判斷空郊,如果是大對(duì)象(指的是占據(jù)了一個(gè)大量的連續(xù)內(nèi)存空間的對(duì)象,如數(shù)組)則直接進(jìn)入老年代如果不是大對(duì)象則直接進(jìn)入新生代里的Eden區(qū)域切揭。
關(guān)于本地線程緩沖分配區(qū)域(TLAB)
由于堆內(nèi)存是線程共享區(qū)域狞甚,在每次為對(duì)象進(jìn)行內(nèi)存空間分配的時(shí)候需要加鎖操作,可以知道的是這個(gè)操作的開銷是比較大的廓旬。所以針對(duì)這種情況哼审,Sun Hotspot JVM為了提高對(duì)象內(nèi)存分配效率,會(huì)為每個(gè)線程在堆內(nèi)存區(qū)域開辟一個(gè)專屬各個(gè)線程的緩存分配區(qū)域(Thread Local Allocation Buffer)孕豹,在這個(gè)區(qū)域進(jìn)行對(duì)象內(nèi)存分配的時(shí)候是不用加鎖的涩盾,所以效率都是很高的。但如果對(duì)象過大的話則仍然是直接使用堆空間分配励背。TLAB僅作用于新生代的Eden Space春霍,因此在編寫Java程序時(shí),通常多個(gè)小的對(duì)象比大的對(duì)象分配起來更加高效叶眉。
2. 幾種內(nèi)存分配策略
- 對(duì)象優(yōu)先被分配到Eden區(qū)域
- 如果是大對(duì)象址儒,則直接分配到老年代區(qū)
- 根據(jù)對(duì)象的年齡(每經(jīng)歷一次GC,則存活下來的對(duì)象的年齡+1)衅疙,如果年齡超過標(biāo)準(zhǔn)(默認(rèn)是15)莲趣,則對(duì)象進(jìn)入老年代。
- Survivor區(qū)域里的相同年齡數(shù)的對(duì)象數(shù)量超過了Survivor區(qū)域里可容納對(duì)象的數(shù)量的一半饱溢,則該些對(duì)象進(jìn)入老年代喧伞。
- 對(duì)象分配空間擔(dān)保機(jī)制
在執(zhí)行一次minorGC的時(shí)候會(huì)檢查一下老年代連續(xù)可用的內(nèi)存空間是否大于新生代所有對(duì)象的大小(防止新生代全部晉升對(duì)象到老年代),如果大于則表示本次minorGC是安全的理朋。如果不是絮识,則會(huì)進(jìn)行一次預(yù)測(cè),根據(jù)以往minorGC結(jié)束后新生代活下來的對(duì)象的平均數(shù)大小是否超過了老年代最大連續(xù)空閑空間嗽上,如果沒超過則表示雖然minorGC有風(fēng)險(xiǎn)但是還會(huì)執(zhí)行次舌,如果超過了則啟動(dòng)majorGC(fullGC)對(duì)老年代進(jìn)行GC回收騰出空間以方便給新生代做空間擔(dān)保。
分配擔(dān)保是老年代為新生代作擔(dān)保兽愤。由于新生代中使用“復(fù)制”算法實(shí)現(xiàn)垃圾回收彼念,老年代中使用“標(biāo)記-清除”或“標(biāo)記-整理”算法實(shí)現(xiàn)垃圾回收,只有使用“復(fù)制”算法的區(qū)域才需要分配擔(dān)保浅萧,因此新生代需要分配擔(dān)保逐沙,而老年代不需要分配擔(dān)保。
3. 對(duì)象內(nèi)存分配的倆種方法
為對(duì)象進(jìn)行內(nèi)存空間分配的任務(wù)洼畅,其實(shí)就是將一塊確定大小的內(nèi)存空間劃走一片吩案。
指針碰撞
假設(shè)Java堆中內(nèi)存是絕對(duì)規(guī)整的,所有用過的內(nèi)存都放在一邊帝簇,空閑的內(nèi)存放在另一邊徘郭,中間放著一個(gè)指針作為分界點(diǎn)的指示器靠益,那所分配內(nèi)存就僅僅是把那個(gè)指針向空閑空間那邊挪動(dòng)一段與對(duì)象大小相等的距離,這種分配方式稱為“指針碰撞”(Bump the Pointer)空閑列表
(CMS這種基于Mark-Sweep算法的收集器) 如果Java堆中的內(nèi)存并不是規(guī)整的残揉,已使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò)胧后,那就沒有辦法簡(jiǎn)單地進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)一個(gè)列表抱环,記錄上哪些內(nèi)存塊是可用的壳快,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的記錄镇草,這種分配方式稱為“空閑列表”(Free List)眶痰。
選擇哪種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定陶夜。因此凛驮,在使用Serial裆站、ParNew等帶Compact過程的收集器時(shí)条辟,系統(tǒng)采用的分配算法是指針碰撞,而使用CMS這種基于Mark-Sweep算法的收集器時(shí)宏胯,通常采用空閑列表
-
關(guān)于GC
什么是GC羽嫡?字面意思解釋就是垃圾回收。在我們Java里面肩袍,當(dāng)對(duì)象創(chuàng)建好以后杭棵,我們是不需要關(guān)心對(duì)象的回收工作的,由JVM虛擬機(jī)會(huì)自動(dòng)幫我們?nèi)セ厥者@些對(duì)象氛赐,而JVM能這樣做的原因就是因?yàn)檫@個(gè)GC垃圾回收機(jī)制魂爪。
1. 確定對(duì)象為垃圾的2種算法
- 引用計(jì)數(shù)法
Jvm會(huì)為每個(gè)對(duì)象進(jìn)行引用計(jì)數(shù),如果一個(gè)對(duì)象被引用了這計(jì)數(shù)加1艰管,如果該引用被釋放了滓侍,則計(jì)數(shù)減1,當(dāng)計(jì)數(shù)為0的時(shí)候則表示該對(duì)象可以被回收牲芋。缺點(diǎn):無法識(shí)別循環(huán)引用的對(duì)象撩笆。
給出例子:
public class ReferenceCountingGC {
public Object instance;
public ReferenceCountingGC(String name){}
}
public static void testGC(){
ReferenceCountingGC a = new ReferenceCountingGC("objA");
ReferenceCountingGC b = new ReferenceCountingGC("objB");
a.instance = b;
b.instance = a;
a = null;
b = null;
}
1. 定義2個(gè)對(duì)象
2. 相互引用
3. 置空各自的聲明引用
我們可以看到,最后這2個(gè)對(duì)象已經(jīng)不可能再被訪問了缸浦,但由于他們相互引用著對(duì)方夕冲,導(dǎo)致它們的引用計(jì)數(shù)永遠(yuǎn)都不會(huì)為0,通過引用計(jì)數(shù)算法裂逐,也就永遠(yuǎn)無法通知GC收集器回收它們歹鱼。
-
可達(dá)性分析算法
根據(jù)GCroots來作為探索起點(diǎn)來探索到對(duì)象之間是否有可到達(dá)的路徑,如果沒有路徑這表示該對(duì)象是不可達(dá)(不可達(dá)對(duì)象不代表是可回收對(duì)象在這之間會(huì)有倆次標(biāo)記過程卜高,倆次標(biāo)記以后任然是可回收對(duì)象則才是可回收對(duì)象)
哪些是GC roots弥姻?
根據(jù)JVM規(guī)定只有虛擬機(jī)方法棧上和本地方法棧上引用的對(duì)象和方法區(qū)中類的靜態(tài)屬性引用的對(duì)象以及方法區(qū)中常量引用的對(duì)象
如何找到GC roots秩霍?
通過采用一個(gè)OopMap的數(shù)據(jù)結(jié)構(gòu)來記錄系統(tǒng)中存活的“GC Roots”,在類加載完成的時(shí)候蚁阳,虛擬機(jī)就把對(duì)象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計(jì)算出來保存在OopMap铃绒,通過OopMap就可以找到堆中的對(duì)象,這些對(duì)象就是GC Roots螺捐。而不需要一個(gè)一個(gè)的去判斷某個(gè)內(nèi)存位置的值是不是引用颠悬。這種方式也叫準(zhǔn)確式GC
2. GC的分類
GC可以被劃分為minorGC和majorGC。第一個(gè)是用來處理新生代區(qū)內(nèi)的對(duì)象回收的GC定血,第二個(gè)是用來處理老年代和永久代(jdk8以后就沒有永久代了)區(qū)內(nèi)的對(duì)象回收的GC赔癌。至于Full GC網(wǎng)上眾說紛壇我看了好幾篇博客大概感覺fullGC應(yīng)該可以理解為minorGC+majorGC。
堆內(nèi)存中何時(shí)觸發(fā)GC進(jìn)行工作的澜沟?
minorGC 當(dāng)Eden區(qū)的大小滿了以后會(huì)觸發(fā)minorGC來進(jìn)行工作灾票;
majorGC 當(dāng)old區(qū)滿了以后會(huì)觸發(fā)majorGC來清理old區(qū)的對(duì)象,或者當(dāng)老年代無法為新生代提供空間擔(dān)保的時(shí)候則會(huì)觸發(fā)majorGC來清理老年代對(duì)象為新生代騰出空間茫虽。
3. 垃圾回收算法
- 標(biāo)記清除算法(mark-sweep)
最基礎(chǔ)的回收算法刊苍,在標(biāo)記階段對(duì)可回收對(duì)象進(jìn)行標(biāo)記,在清除階段對(duì)被標(biāo)記的對(duì)象所占空間進(jìn)行回收濒析。缺點(diǎn):內(nèi)存碎片化嚴(yán)重正什,可能會(huì)導(dǎo)致大的對(duì)象找不到內(nèi)存空間
復(fù)制算法(coping)
將內(nèi)存空間分為大小相等的倆塊,當(dāng)其中一塊內(nèi)存空間已經(jīng)滿了的時(shí)候号杏,將不可回收的對(duì)象復(fù)制到另一塊內(nèi)存空間中婴氮,然后將原先滿了的內(nèi)存空間清理干凈,再次使用內(nèi)存空間進(jìn)行分配的時(shí)候就使用另一塊內(nèi)存空間盾致。這樣的好處是避免了上述算法會(huì)產(chǎn)生大量的內(nèi)存碎片空間主经,缺點(diǎn):每次只能使用一半的內(nèi)存,而且如果存活對(duì)象比較多的情況會(huì)導(dǎo)致復(fù)制算法效率下降(大量的存活對(duì)象需要進(jìn)行復(fù)制遷移)標(biāo)記壓縮算法(mark-compact)
綜合以上倆種方法的缺陷庭惜,提出第三種回收算法罩驻,標(biāo)記階段與標(biāo)記清除算法是差不多的,然后標(biāo)記結(jié)束不是立即回收蜈块,而是將存活的對(duì)象移動(dòng)到內(nèi)存的一端鉴腻,然后再清除端邊界外的對(duì)象。好處:解決了上述倆種方法的內(nèi)存空間碎片化嚴(yán)重百揭、內(nèi)存空間利用不足的問題爽哎。分代收集算法
分代收集法是目前大部分 JVM 所采用的方法,老年代的特點(diǎn)是每次垃圾回收時(shí)只有少量對(duì)象需要被回收器一,新生代的特點(diǎn)是每次垃圾回收時(shí)都有大量垃圾需要被回收课锌,因此可以根據(jù)不同區(qū)域選擇不同的算法。新生代都采取復(fù)制算法,老年代采取標(biāo)記壓縮算法渺贤。
為什么堆內(nèi)存要分區(qū)雏胃?
是為了更好的對(duì)堆內(nèi)對(duì)象進(jìn)行回收工作,在新生代中針對(duì)對(duì)象的朝生夕死特性志鞍,選擇使用復(fù)制算法來進(jìn)行GC工作瞭亮,因?yàn)榇婊畹膶?duì)象少所以復(fù)制算法的效率很高。在老年代中采用的是標(biāo)記壓縮法來進(jìn)行固棚,因?yàn)榇婊畹膶?duì)象比較多如果采用復(fù)制算法則會(huì)導(dǎo)致效率很低统翩。
4. 幾種垃圾收集器
- Serial收集器(新生代的垃圾收集器)
單線程、采用復(fù)制算法的垃圾收集器此洲。它不但只會(huì)使用一個(gè) CPU 或一條線程去完成垃圾收集工作厂汗,并且在進(jìn)行垃圾收集的同時(shí),必須暫停其他所有的工作線程(STOP THE WORLD)呜师,直到垃圾收集結(jié)束娶桦。
Serial 垃圾收集器雖然在收集垃圾過程中需要暫停所有其他的工作線程,但是它簡(jiǎn)單高效汁汗,是jvm運(yùn)行在client模式下的默認(rèn)采用的新生代垃圾收集器衷畦。
通過-XX:+UseSerialGC參數(shù)啟用
ParNew收集器(新生代的垃圾收集器)
與serial垃圾收集器唯一的不同在于它是多線程下的采用復(fù)制算法的垃圾收集器。但對(duì)于單核的設(shè)備來說碰酝,需要進(jìn)行線程之間的切換霎匈,效率反而沒有單線程的高。默認(rèn)開啟的線程數(shù)等于cpu的數(shù)量送爸。Jvm運(yùn)行在server模式下默認(rèn)采用的新生代垃圾收集器。
通過-XX:ParallelGCThreads參數(shù)限制收集的線程數(shù)暖释,-XX:+UseParNewGC參數(shù)啟用Parallel Scavenge收集器(新生代的垃圾收集器)
與PN相同的是袭厂,同樣使用復(fù)制算法,也是一個(gè)多線程的垃圾收集器球匕。與PN不同點(diǎn)在于它關(guān)注程序是否到達(dá)了一個(gè)可控制的吞吐量(CPU計(jì)算用戶代碼消耗時(shí)間/cpu總消耗時(shí)間)主要追求高效的CPU計(jì)算纹磺,追求快速完成計(jì)算任務(wù)。(適用于在后臺(tái)運(yùn)算不需要太多交互的任務(wù))
通過參數(shù)-XX:MaxGCPauseMillis設(shè)置最大GC的停頓時(shí)間和-XX:GCTimeRatio 設(shè)置吞吐量的大小亮曹。-XX:+UseParallelGC參數(shù)啟用
另外橄杨,可以通過-XX:+UseAdaptiveSizePolicy參數(shù)開啟自適應(yīng)調(diào)節(jié)策略,這樣可以免去我們自己設(shè)置堆內(nèi)存的一些細(xì)節(jié)參數(shù)照卦,比如新生代內(nèi)存大小式矫,Eden與Survivor之間的比例等等。這個(gè)參數(shù)適合對(duì)內(nèi)存手工優(yōu)化存在困難的時(shí)候使用役耕,他能監(jiān)控系統(tǒng)當(dāng)前的狀態(tài)采转,動(dòng)態(tài)的調(diào)整以達(dá)到最大的吞吐量
Serial Old垃圾收集器(老年代的垃圾收集器)
單線程下采用標(biāo)記整理算法來實(shí)現(xiàn)。也是在client模式下使用的默認(rèn)老年代垃圾收集器瞬痘。在server模式下作為CMS并發(fā)收集模式失敗的情況下備選方案故慈。Parallel Old垃圾收集器(老年代的垃圾收集器)
是SO的多線程版本板熊。同時(shí)也保證了老年代的吞吐量與PS在新生代里保證吞吐量的功能是一致的。-
CMS垃圾收集器(老年代的垃圾收集器)
是多線程下采用標(biāo)記清除算法來實(shí)現(xiàn)的察绷。主要是為了追求最短的回收停頓時(shí)間來快速響應(yīng)服務(wù)干签。
步驟:
初始標(biāo)記:需要進(jìn)行STW的,但僅僅只是標(biāo)記GC Roots能夠直接關(guān)聯(lián)的對(duì)象(并不是死掉的對(duì)象哦~)拆撼,由于有OopMap的存在筒严,因此該步驟速度非常快情萤。如圖鸭蛙,其中藍(lán)色底紋的便是能夠直接關(guān)聯(lián)的對(duì)象。
并發(fā)標(biāo)記:這步是不需要STW的筋岛,不需要娶视!他和我們的主程序線程共同執(zhí)行,從上一步被標(biāo)記的對(duì)象開始睁宰,進(jìn)行可達(dá)性分析組成“關(guān)系網(wǎng)”肪获。由于不需要進(jìn)行SWT,所以該步驟不會(huì)影響用戶體驗(yàn)柒傻。既然不暫停線程孝赫,大家是不是又怕回收了不該回收的對(duì)象?為了避免這個(gè)問題红符,因此就有了第三步青柄。
重新標(biāo)記:針對(duì)并發(fā)標(biāo)記過程中因?yàn)槌绦蚶^續(xù)運(yùn)行,導(dǎo)致標(biāo)記發(fā)生變化的那部分對(duì)象進(jìn)行標(biāo)記修正预侯,需要暫停所有的工作線程致开。
并發(fā)清除:
清除GC roots里不可達(dá)對(duì)象,和用戶線程一起工作不需要暫停工作進(jìn)程萎馅。由于四個(gè)階段里耗時(shí)最長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除都不需要暫停工作進(jìn)程双戳,所以可以認(rèn)為CMS的垃圾回收是和用戶線程一起并發(fā)進(jìn)行的。
優(yōu)點(diǎn):并發(fā)收集垃圾糜芳,低停頓性飒货。
缺點(diǎn):由于采用標(biāo)記-清除算法所以會(huì)產(chǎn)生內(nèi)存碎片,對(duì)于浮動(dòng)垃圾沒辦法收集峭竣。(當(dāng)在并發(fā)清除垃圾的時(shí)候塘辅,也就是第四步的時(shí)候,他是與當(dāng)前主線程并發(fā)執(zhí)行的邪驮,因此他在回收的時(shí)候莫辨,我們的主線程又會(huì)產(chǎn)生新的垃圾,而這些垃圾在這次回收過程已經(jīng)回收不了了,只能等待下一次回收了沮榜。這些垃圾又叫做“浮動(dòng)垃圾”) G1垃圾收集器(作用范圍為整個(gè)堆的垃圾收集器)
相對(duì)于CMS垃圾收集器來說盘榨,G1收集器采用標(biāo)記-壓縮算法不會(huì)產(chǎn)生內(nèi)存碎片。而且能更加精準(zhǔn)的控制停頓時(shí)間蟆融,實(shí)現(xiàn)低停頓垃圾回收草巡。(主要是采用了分區(qū),針對(duì)每個(gè)區(qū)的垃圾收集進(jìn)度情況來在后臺(tái)維護(hù)一個(gè)優(yōu)先級(jí)列表型酥,每次根據(jù)所允許的收集時(shí)間山憨,優(yōu)先選擇垃圾回收最多的區(qū)域)通過-XX:MaxGCPauseMills設(shè)置有限的收集時(shí)間。
垃圾收集器是不能隨意組合的弥喉,現(xiàn)在給出垃圾收集器互相的組合圖:
至此郁竟,就把堆內(nèi)存與GC相關(guān)的東西說完了,有點(diǎn)淺顯由境,很多都是泛泛而談棚亩。后面我會(huì)專開一篇文章來實(shí)戰(zhàn)分析。