《深入理解Java虛擬機(jī)》——垃圾收集器與內(nèi)存分配策略問題

通過這篇文章你能知道的問題:

1.如何判斷對(duì)象是活著還是死去?

2.在Java語言中,可作為GCRoots的對(duì)象有哪些?

3.Java中引用的分類

4.對(duì)象的自救姿勢(shì)是什么?

5.類在什么情況下是無用的?

6.垃圾收集算法有哪些?

7.年輕代,老年代,永久代?

8.HotSpot虛擬機(jī)是如何發(fā)起內(nèi)存回收的?

9.垃圾收集器有哪些以及組合方式有哪些?

10.怎么理解GC日志?

11.內(nèi)存如何分配?

該篇文章的篇幅會(huì)有點(diǎn)長(zhǎng),希望大家耐心閱讀下去。

大家都知道內(nèi)存回收的是無用對(duì)象,但是怎么判斷對(duì)象是無用還是有用呢?

比較容易想到的是引用計(jì)數(shù)法。那么什么是引用計(jì)數(shù)法呢?

引用計(jì)數(shù)法:給對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器值就加1;當(dāng)引用失效時(shí),計(jì)數(shù)器值就減1;任何時(shí)刻計(jì)數(shù)器為0的對(duì)象就是不可能再使用的。

雖然引用計(jì)數(shù)算法實(shí)現(xiàn)簡(jiǎn)單且判斷效率高效,但是在主流的Java虛擬機(jī)里面沒有選用引用計(jì)數(shù)算法來管理內(nèi)存,其中最主要的原因是它很難解決對(duì)象間相互循環(huán)引用的問題。

既然我們學(xué)習(xí)的是Java虛擬機(jī)相關(guān)的內(nèi)容,那我們就要了解Java虛擬機(jī)的實(shí)現(xiàn)中是怎么判斷對(duì)象是否存活的?

可達(dá)性分析算法:在主流的商用程序語言的主流實(shí)現(xiàn)中,都是稱通過可達(dá)性分析來判斷對(duì)象是否存活的。這個(gè)算法的基本思路就是通過一系列的稱為"GC Roots"的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑稱為引用鏈,當(dāng)一個(gè)對(duì)象到GC Roots沒有任何引用鏈相連時(shí),則證明此對(duì)象是不可用的。如圖3-1所示:

在Java語言中,可作為GC Roots的對(duì)象包括下面幾種:

虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象

方法區(qū)中類靜態(tài)屬性引用的對(duì)象

方法區(qū)中常量引用的對(duì)象

本地方法棧中JNI(即一般說的Native方法)引用的對(duì)象

無論是通過引用計(jì)數(shù)算法判斷對(duì)象的引用數(shù)量,還是通過可達(dá)性分析算法判斷對(duì)象的引用鏈?zhǔn)欠窨蛇_(dá),判斷對(duì)象是否存活都與"引用"有關(guān)。

在JDK1.2之后,Java對(duì)引用的概念進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強(qiáng)度依次逐漸減弱。

強(qiáng)引用就是指在程序代碼之中普遍存在的,類似"Object obj=new Object()"這類的引用,只要強(qiáng)引用還存在,垃圾收集器永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象。

軟引用是用來描述一些還有用但并非必需的對(duì)象。對(duì)于軟引用關(guān)聯(lián)著的對(duì)象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會(huì)把這些對(duì)象列進(jìn)回收范圍之中進(jìn)行第二次回收。如果這次回收還沒有足夠的內(nèi)存,才會(huì)拋出內(nèi)存溢出異常。

弱引用也是用來描述非必需對(duì)象的,但是它的強(qiáng)度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生之前。當(dāng)垃圾收集器工作時(shí),無論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象。

虛引用也被稱為幽靈引用或者幻影引用,它是最弱的一種引用關(guān)系。一個(gè)對(duì)象是否有虛引用的存在,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無法通過虛引用來取得一個(gè)對(duì)象實(shí)例。為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知。

上面介紹了對(duì)象存活的判斷以及引用的分類,那么對(duì)象如何在被殺死時(shí)來個(gè)自我救贖呢?

救贖之前我們要了解我們的對(duì)象是怎么被殺死的:

在可達(dá)性分析算法中不可達(dá)的對(duì)象,也并非是“非死不可”的,這時(shí)候它們暫時(shí)處于“緩刑”階段,要真正宣告一個(gè)對(duì)象死亡,至少要經(jīng)歷兩次標(biāo)記過程——如果對(duì)象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈,那它將會(huì)被第一次標(biāo)記并且進(jìn)行一次篩選,篩選的條件是此對(duì)象是否有必要執(zhí)行finalize()方法。當(dāng)對(duì)象沒有覆蓋finalize()方法,或者finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過,虛擬機(jī)將這兩種情況都視為“沒有必要執(zhí)行”。(那么問題來了,這種情況下的二次標(biāo)記在哪里?)

如果這個(gè)對(duì)象被判定為有必要執(zhí)行finalize()方法,那么這個(gè)對(duì)象將會(huì)放置在一個(gè)叫做F-Queue的隊(duì)列之中,并在稍后由一個(gè)由虛擬機(jī)自動(dòng)建立的、低優(yōu)先級(jí)的Finalizer線程去執(zhí)行它。這里所謂的"執(zhí)行"是指虛擬機(jī)會(huì)觸發(fā)這個(gè)方法,但并不承諾會(huì)等待它運(yùn)行結(jié)束,這樣做的原因是,如果一個(gè)對(duì)象在finalize()方法中執(zhí)行緩慢,或者發(fā)生了死循環(huán),將很可能會(huì)導(dǎo)致F-Queue隊(duì)列中其他對(duì)象永久處于等待,甚至導(dǎo)致整個(gè)內(nèi)存回收系統(tǒng)崩潰。finalize()方法是對(duì)象逃脫死亡命運(yùn)的最后一次機(jī)會(huì),稍后GC將對(duì)F-Queue中的對(duì)象進(jìn)行第二次小規(guī)模標(biāo)記,如果對(duì)象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個(gè)對(duì)象建立關(guān)聯(lián)即可,譬如把自己(this關(guān)鍵字)賦值給某個(gè)類變量或者對(duì)象的成員變量,那在第二次標(biāo)記時(shí)它將被移除出"即將回收"的集合;如果對(duì)象這時(shí)候還沒有逃脫,那基本上它就真的被回收了。

[java]view plaincopy

packagecom.general.android.test.oom;

publicclassTestFinalize?{

publicstaticTestFinalize?SAVE_HOOK=null;

publicvoidisAlive(){

System.out.println("yes,?i?am?still?alive");

}

@Override

protectedvoidfinalize()throwsThrowable?{

//?TODO?Auto-generated?method?stub

super.finalize();

System.out.println("finalize?method?executed");

TestFinalize.SAVE_HOOK=this;

}

publicstaticvoidmain(String[]?args)throwsInterruptedException?{

SAVE_HOOK=newTestFinalize();

//對(duì)象第一次成功拯救自己

SAVE_HOOK=null;

System.gc();

Thread.sleep(500);

if(SAVE_HOOK!=null){

SAVE_HOOK.isAlive();

}else{

System.out.println("no,I?am?dead");

}

SAVE_HOOK=null;

System.gc();

Thread.sleep(500);

if(SAVE_HOOK!=null){

SAVE_HOOK.isAlive();

}else{

System.out.println("no,I?am?dead");

}

}

}

上面代碼的輸出結(jié)果:

finalize method executed

yes, i am still alive

no,I am dead

注意:任何一個(gè)對(duì)象的finalize()方法都只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次,如果對(duì)象面臨下一次回收,它的finalize()方法不會(huì)被再次執(zhí)行。

在方法區(qū)中類需要同時(shí)滿足下面3個(gè)條件才能算是“無用的類”:

1.該類所有的實(shí)例都已經(jīng)被回收,也就是Java堆中不存在該類的任何實(shí)例

2.加載該類的ClassLoader已經(jīng)被回收

3.該類對(duì)應(yīng)的java.ang.Class對(duì)象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

虛擬機(jī)可以對(duì)滿足上述3個(gè)條件的無用類進(jìn)行回收,這里說的僅僅是“可以”,而并不是和對(duì)象一樣,不使用了就必然會(huì)回收。是否對(duì)類進(jìn)行回收,HotSpot虛擬機(jī)提供了-Xnoclassgc參數(shù)進(jìn)行控制,還可以使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看類加載和卸載信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虛擬機(jī)中使用,-XX:+TraceClassUnLoading參數(shù)需要FastDebug版的虛擬機(jī)支持。

常用的垃圾收集算法有哪些?

常用的垃圾收集算法有:標(biāo)記-清除算法、復(fù)制算法、標(biāo)記-整理算法、分代收集算法。

標(biāo)記-清除算法:算法分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對(duì)象。標(biāo)記-清除算法主要有兩個(gè)不足:(1)一個(gè)是效率問題,標(biāo)記和清除兩個(gè)過程的效率都不高;另一個(gè)是空間問題,標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致以后在程序運(yùn)行過程中需要分配較大對(duì)象時(shí),無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。

復(fù)制算法:復(fù)制算法將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。這樣使得每次都是對(duì)整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收,內(nèi)存分配時(shí)也就不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動(dòng)堆頂指針,按順序分配內(nèi)存即可,實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效。只是這種算法的代價(jià)是將內(nèi)存縮小為了原來的一半,未免太高了一點(diǎn)。

現(xiàn)在的商業(yè)虛擬機(jī)都采用這種收集算法來回收新生代,IBM公司的專門研究表明,新生代中的對(duì)象98%是"朝生夕死"的,所以并不需要按照1:1的比例來劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當(dāng)回收時(shí),將Eden和Survivor中還存活的對(duì)象一次性地復(fù)制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8:1:1,也就是每次新生代中可用內(nèi)存空間為整個(gè)新生代容量的90%(80%+10%),只有10%的內(nèi)存會(huì)被“浪費(fèi)”。當(dāng)然,98%的對(duì)象可回收只是一般場(chǎng)景下的數(shù)據(jù),我們沒有辦法保證每次回收都只有不多于10%的對(duì)象存活,當(dāng)Survivor空間不夠用時(shí),需要依賴其他內(nèi)存(這里指老年代)進(jìn)行分配擔(dān)保,也就是說如果另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對(duì)象時(shí),這些對(duì)象將直接通過分配擔(dān)保機(jī)制進(jìn)入老年代。

標(biāo)記-整理算法:復(fù)制收集算法在對(duì)象存活率較高時(shí)就要進(jìn)行較多的復(fù)制操作,效率將會(huì)變低。更關(guān)鍵的是,如果不想浪費(fèi)50%的空間,就需要有額外的空間進(jìn)行分配擔(dān)保,以應(yīng)對(duì)被使用的內(nèi)存中所有對(duì)象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。根據(jù)老年代的特點(diǎn),有人提出了另外一種“標(biāo)記-整理”算法,標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。

分代收集算法:當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都采用"分代收集"算法,這種算法并沒有什么新的思想,只是根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊。一般把Java堆分為新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最恰當(dāng)?shù)氖占惴āT谛律校看卫占瘯r(shí)都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集。而老年代中因?yàn)閷?duì)象存活率高,沒有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用標(biāo)記-清理或者標(biāo)記-整理算法來進(jìn)行回收。

這里提到了新生代和老年代,那么新生代是什么?老年代又是什么呢?永久代又是何物呢?

新生代

主要是用來存放新生的對(duì)象,即剛剛創(chuàng)建的對(duì)象。新生代又被劃分為Eden區(qū)和Survivor區(qū)(包含空間相等的From、To區(qū),沒有先后順序,是復(fù)制算法的需要)。大多數(shù)情況下,java中新建的對(duì)象都是在新生代上分配的,通過復(fù)制算法來進(jìn)行分配內(nèi)存和垃圾回收。

老年代

主要存放應(yīng)用程序中生命周期長(zhǎng)的內(nèi)存對(duì)象。在新生代中經(jīng)過多次垃圾回收仍然存活的對(duì)象,會(huì)被存放到老年代中。老年代通過標(biāo)記-整理算法來清理無用內(nèi)存。

永久代

永久代這個(gè)概念比較特殊,因?yàn)橛谰么荋otspot虛擬機(jī)特有的概念,是方法區(qū)的一種實(shí)現(xiàn),別的JVM都沒有這個(gè)東西。但是在jdk1.8中,永久代已經(jīng)被移除。

在Java 6中,方法區(qū)中包含的數(shù)據(jù),除了JIT編譯生成的代碼存放在native memory的CodeCache區(qū)域,其他都存放在永久代;

在Java 7中,Symbol的存儲(chǔ)從PermGen移動(dòng)到了native memory,并且把靜態(tài)變量從instanceKlass末尾(位于PermGen內(nèi))移動(dòng)到了java.lang.Class對(duì)象的末尾(位于普通Java heap內(nèi));

在Java 8中,永久代被徹底移除,取而代之的是另一塊與堆不相連的本地內(nèi)存——元空間(Metaspace),?XX:MaxPermSize 參數(shù)失去了意義,取而代之的是-XX:MaxMetaspaceSize。

http://blog.csdn.net/ooppookid/article/details/51477147

https://www.zhihu.com/question/49044988/answer/113961406

HotSpot是如何去發(fā)起內(nèi)存回收的?

前面說過HotSpot虛擬機(jī)是通過可達(dá)性分析來判斷對(duì)象是否存活的。而垃圾收集算法在標(biāo)記可回收對(duì)象時(shí),肯定也是使用了可達(dá)性分析來判斷哪些對(duì)象是可以被回收。大家都知道可作為GC Roots的節(jié)點(diǎn)主要在全局性的引用(例如常量或類靜態(tài)屬性)與執(zhí)行上下文(例如棧幀中的本地變量表)中,現(xiàn)在很多應(yīng)用僅僅方法區(qū)就有數(shù)百兆,如果要逐個(gè)檢查這里面的引用,那么必然會(huì)消耗很多時(shí)間。另外,可達(dá)性分析對(duì)執(zhí)行時(shí)間的敏感還體現(xiàn)在GC停頓上,因?yàn)檫@項(xiàng)分析工作必須在一個(gè)能確保一致性的快照中進(jìn)行——這里"一致性"的意思是指整個(gè)分析期間整個(gè)執(zhí)行系統(tǒng)看起來就像被凍結(jié)在某個(gè)時(shí)間點(diǎn)上,不可以出現(xiàn)分析過程中對(duì)象引用關(guān)系還在不斷變化的情況,該點(diǎn)不滿足的話分析結(jié)果準(zhǔn)確性就無法得到保證。這點(diǎn)是導(dǎo)致GC進(jìn)行時(shí)必須停頓所有Java執(zhí)行線程的其中一個(gè)重要原因,即使是在號(hào)稱不會(huì)發(fā)生停頓的CMS收集器中,枚舉根節(jié)點(diǎn)時(shí)也是必須要停頓的。

由于目前的主流Java虛擬機(jī)使用的都是準(zhǔn)確式GC,所以當(dāng)執(zhí)行系統(tǒng)停頓下來后,并不需要一個(gè)不漏地檢查完所有執(zhí)行上下文和全局的引用位置,虛擬機(jī)應(yīng)當(dāng)是有辦法直接得知哪些地方存放著對(duì)象引用。在HotSpot的實(shí)現(xiàn)中,是使用一組稱為OopMap的數(shù)據(jù)結(jié)構(gòu)來達(dá)到這個(gè)目的,在類加載完成的時(shí)候,HotSpot就把對(duì)象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計(jì)算出來,在JIT編譯過程中,也會(huì)在特定的位置記錄下棧和寄存器中哪些位置是引用。這些位置稱為安全點(diǎn)(Safepoint),即程序執(zhí)行時(shí)并非在所有地方都能停頓下來開始GC,只有在到達(dá)安全點(diǎn)時(shí)才能暫停。Safepoint的選定既不能太少以致于讓GC等待時(shí)間太長(zhǎng),也不能過于頻繁以致于過分增大運(yùn)行時(shí)的負(fù)荷。所以,安全點(diǎn)的選定基本上是以程序“是否具有讓程序長(zhǎng)時(shí)間執(zhí)行的特征”為標(biāo)準(zhǔn)進(jìn)行選定的。對(duì)于安全點(diǎn),另一個(gè)需要考慮的問題是如何在GC發(fā)生時(shí)讓所有線程(這里不包括執(zhí)行JNI調(diào)用的線程)都“跑”到最近的安全點(diǎn)上再停頓下來。這里有兩種方案可供選擇:搶先式中斷和主動(dòng)式中斷。

搶先式中斷:不需要線程的執(zhí)行代碼主動(dòng)去配合,在GC發(fā)生時(shí),首先把所有線程全部中斷,如果發(fā)現(xiàn)有線程中斷的地方不在安全點(diǎn)上,就恢復(fù)線程,讓它“跑”到安全點(diǎn)上。但是現(xiàn)在幾乎沒有虛擬機(jī)實(shí)現(xiàn)采用搶先式中斷來暫停線程從而響應(yīng)GC事件。

主動(dòng)式中斷:當(dāng)GC需要中斷線程的時(shí)候,不直接對(duì)線程操作,僅僅簡(jiǎn)單地設(shè)置一個(gè)標(biāo)志,各個(gè)線程執(zhí)行時(shí)主動(dòng)去輪詢這個(gè)標(biāo)志,發(fā)現(xiàn)中斷標(biāo)志為真時(shí)就自己中斷掛起。輪詢標(biāo)志的地方和安全點(diǎn)是重合的,另外再加上創(chuàng)建對(duì)象需要分配內(nèi)存的地方。

使用安全點(diǎn)似乎已經(jīng)完美地解決了如何進(jìn)入GC的問題,但是實(shí)際情況卻并不一定。安全點(diǎn)機(jī)制保證了程序執(zhí)行時(shí),在不太長(zhǎng)的時(shí)間內(nèi)就會(huì)遇到可進(jìn)入GC的Safepoint。但是在程序不執(zhí)行的時(shí)候就無法做到這一點(diǎn),比如線程在休眠或阻塞狀態(tài)。對(duì)于這種情況,就需要安全區(qū)域(Safe Region)來解決。

安全區(qū)域是指在一段代碼片段之中,引用關(guān)系不會(huì)發(fā)生變化。在這個(gè)區(qū)域中的任意地方開始GC都是安全的。我們也可以把Safe Region看做是被擴(kuò)展了的Safepoint。

在線程執(zhí)行到Safe Region中的代碼時(shí),首先標(biāo)識(shí)自己已經(jīng)進(jìn)入了Safe Region,那樣,當(dāng)在這段時(shí)間里JVM要發(fā)起GC時(shí),就不用管標(biāo)識(shí)自己為Safe Region狀態(tài)的線程了。在線程要離開SafeRegion時(shí),它要檢查系統(tǒng)是否已經(jīng)完成了根節(jié)點(diǎn)枚舉(或者是整個(gè)GC過程),如果完成了,那線程就繼續(xù)執(zhí)行,否則它就必須等待直到收到可以安全離開Safe Region的信號(hào)為止。

這個(gè)闡述有點(diǎn)長(zhǎng),我來簡(jiǎn)要概括一下:在OopMap的協(xié)助下,HotSpot快速且準(zhǔn)確地完成GC Roots枚舉,并在安全點(diǎn)或者安全區(qū)域進(jìn)行中斷,以保證在根結(jié)點(diǎn)枚舉過程中引用關(guān)系不發(fā)生改變,也就是在標(biāo)記可回收對(duì)象時(shí),內(nèi)存中的引用關(guān)系不再發(fā)生變化,這樣的操作才有意義。

垃圾收集器有哪些以及組合方式有哪些?

如果說收集算法是內(nèi)存回收的方法論,那么垃圾收集器就是內(nèi)存回收的具體實(shí)現(xiàn)。Java虛擬機(jī)規(guī)范中對(duì)垃圾收集器應(yīng)該如何實(shí)現(xiàn)并沒有任何規(guī)定,因此不同的廠商、不同的版本的虛擬機(jī)所提供的垃圾收集器都可能會(huì)有很大的差別,并且一般都會(huì)提供參數(shù)供用戶自己根據(jù)自己的應(yīng)用特點(diǎn)和要求組合出各個(gè)年代所使用的收集器。

這里所討論的收集器是基于JDK1.7Update 14之后的HotSpot虛擬機(jī),上圖展示了7種作用于不同分代的收集器,如果兩個(gè)收集器之間存在連線,就說明它們可以搭配使用。虛擬機(jī)所處的區(qū)域,則表示它是屬于新生代收集器還是老年代收集器。

Serial收集器

Serial收集器是最基本、發(fā)展歷史最悠久的收集器,曾經(jīng)(在JDK1.3.1之前)是虛擬機(jī)新生代收集的唯一選擇。大家看名字就會(huì)知道,這個(gè)收集器是一個(gè)單線程的收集器,但它的“單線程”的意義并不僅僅說明它只會(huì)使用一個(gè)CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進(jìn)行垃圾收集時(shí),必須暫停其他所有的工作線程,直到它收集結(jié)束。Serial收集器是虛擬機(jī)運(yùn)行在Client模式下的默認(rèn)新生代收集器,它有著優(yōu)于其他收集器的地方:簡(jiǎn)單而高效(與其他收集器的單線程比),對(duì)于限定單個(gè)CPU的環(huán)境來說,Serial收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。

ParNew收集器

ParNew收集器其實(shí)就是Serial收集器的多線程版本,除了使用多條線程進(jìn)行垃圾收集之外,其余行為包括Serial收集器可用的所有控制參數(shù)、收集算法、Stop The World、對(duì)象分配規(guī)則、回收策略等都與Serial收集器完全一樣,在實(shí)現(xiàn)上,這兩種收集器也共用了相當(dāng)多的代碼。ParNew收集器是許多運(yùn)行在Server模式下的虛擬機(jī)中首先的新生代收集器,其中有一個(gè)與性能無關(guān)但很重要的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作。

在談?wù)摾占鞯纳舷挛恼Z境中,并行和并發(fā)的概念如下:

并行:指多余垃圾收集線程并行工作,但此時(shí)用戶線程仍然處于等待狀態(tài)。

并發(fā):指用戶線程與垃圾收集線程同時(shí)執(zhí)行(但不一定是并行的,可能會(huì)交替執(zhí)行),用戶程序在繼續(xù)運(yùn)行,而垃圾收集程序運(yùn)行于另一個(gè)CPU上。

Parallel Scavenge收集器

Parallel Scavenge收集器是一個(gè)新生代收集器,它也是使用復(fù)制算法的收集器,又是并行的多線程收集器。Parallel Scavenge收集器的特點(diǎn)是它的關(guān)注點(diǎn)與其他收集器不同,CMS等收集器的關(guān)注點(diǎn)是盡可能地縮短垃圾收集時(shí)用戶線程的停頓時(shí)間,而Parallel Scavenge收集器的目標(biāo)則是達(dá)到一個(gè)可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用于運(yùn)行用戶代碼的時(shí)間與CPU總消耗時(shí)間的比值,即吞吐量=運(yùn)行用戶代碼時(shí)間/(運(yùn)行用戶代碼時(shí)間+垃圾收集時(shí)間),虛擬機(jī)總共運(yùn)行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。停頓時(shí)間越短就越適合需要與用戶交互的程序,良好的響應(yīng)速度能提升用戶體驗(yàn),而高吞吐量則可以高效率地利用CPU時(shí)間,盡快完成程序的運(yùn)算任務(wù),主要適合在后臺(tái)運(yùn)算而不需要太多交互的任務(wù)。

Parallel Scavenge的一些參數(shù):

MaxGCPauseMillis:控制最大垃圾收集停頓時(shí)間,參數(shù)允許的值是一個(gè)大于0的毫秒數(shù),收集器將盡可能地保證內(nèi)存回收花費(fèi)的時(shí)間不超過設(shè)定值。不過大家不要認(rèn)為如果把這個(gè)參數(shù)的值設(shè)置得稍小一點(diǎn)就能使得系統(tǒng)的垃圾收集速度變得更快,GC停頓時(shí)間縮短是以犧牲吞吐量和新生代空間來換取的。

GCTimeRatio:控制吞吐量的大小,參數(shù)的值應(yīng)當(dāng)是一個(gè)大于0且小于100的整數(shù),也就是垃圾收集時(shí)間占總時(shí)間的比率,相當(dāng)于是吞吐量的倒數(shù)。如果把此參數(shù)設(shè)置為N,那允許的最大GC時(shí)間就占總時(shí)間的(1/(1+N))%。比如把此參數(shù)設(shè)置為19,那允許的最大GC時(shí)間就占總時(shí)間的5%(即1/(1+19)).

UseAdaptiveSizePolicy:這是一個(gè)開關(guān)參數(shù),當(dāng)這個(gè)參數(shù)打開之后,就不需要手工指定新生代的大小(-Xmn)、Eden與Survivor區(qū)的比例(-XX:SurvivorRatio)、晉升老年代對(duì)象大小(-XX:PretenureSizeThreshold)等細(xì)節(jié)參數(shù)了,虛擬機(jī)會(huì)根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控信息,動(dòng)態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時(shí)間或者最大的吞吐量,這種調(diào)節(jié)方式稱為GC自適應(yīng)的調(diào)節(jié)策略。自適應(yīng)調(diào)節(jié)策略也是Parallel Scavenge收集器與ParNew收集器的一個(gè)重要區(qū)別。

Serial Old收集器

Serial Old是Serial 收集器的老年代版本,它同樣是一個(gè)單線程收集器,使用"標(biāo)記-整理"算法。這個(gè)收集器的主要意義也是在于給Client模式下的虛擬機(jī)使用。如果在Server模式下,那么它主要還有兩大用途:一種用途是在JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另一種用途就是作為CMS收集器的后備預(yù)案,在并發(fā)收集發(fā)生Concurrent Mode Failure時(shí)使用。

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和標(biāo)記-整理算法。這個(gè)收集器是在JDK1.6中才開始提供的。在注重吞吐量以及CPU資源敏感的場(chǎng)合,都可以優(yōu)先考慮Parallel Scavenge加Parallel Old收集器。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。目前很大一部分的Java應(yīng)用集中在互聯(lián)網(wǎng)網(wǎng)站或者B/S系統(tǒng)的服務(wù)端上,這類應(yīng)用尤其重視服務(wù)的響應(yīng)速度,希望系統(tǒng)停頓時(shí)間最短,以給用戶帶來較好的體驗(yàn)。CMS收集器就非常符合這類應(yīng)用的需求。從名字(包含"Mark Sweep")上就可以看出,CMS收集器是基于"標(biāo)記-清除"算法實(shí)現(xiàn)的,它的運(yùn)作過程相對(duì)于前面幾種收集器來說更復(fù)雜一些,整個(gè)過程分為4個(gè)步驟,包括:

初始標(biāo)記(CMS initial mark)

并發(fā)標(biāo)記(CMS concurrent mark)

重新標(biāo)記(CMS remark)

并發(fā)清除(CMS concurrent sweep)

其中,初始標(biāo)記、重新標(biāo)記這兩個(gè)步驟仍然需要"Stop The World"。初始標(biāo)記僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,速度很快,并發(fā)標(biāo)記階段就是進(jìn)行GC Roots Tracing 的過程,而重新標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄,這個(gè)階段的停頓時(shí)間一般會(huì)比初始標(biāo)記階段稍長(zhǎng)一些,但遠(yuǎn)比并發(fā)標(biāo)記的時(shí)間短。由于整個(gè)過程中耗時(shí)最長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除過程收集器收集線程都可以與用戶線程一起工作,所以,從總體上來說,CMS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)執(zhí)行的。

CMS收集器有3個(gè)明顯的缺點(diǎn):

1.CMS收集器對(duì)CPU資源非常敏感。其實(shí),面向并發(fā)設(shè)計(jì)的程序都對(duì)CPU資源比較敏感。在并發(fā)階段,它雖然不會(huì)導(dǎo)致用戶線程停頓,但是會(huì)因?yàn)檎加昧艘徊糠志€程而導(dǎo)致應(yīng)用程序變慢,總吞吐量會(huì)降低。

2.CMS收集器無法處理浮動(dòng)垃圾,可能出現(xiàn)"Concurrent Mode Failure"失敗而導(dǎo)致另一次Full GC的產(chǎn)生。由于CMS并發(fā)清理階段用戶線程還在運(yùn)行著,伴隨程序運(yùn)行自然就還會(huì)有新的垃圾不斷產(chǎn)生,這一部分垃圾出現(xiàn)在標(biāo)記過程之后,CMS無法在當(dāng)次收集中處理掉它們,只好留待下一次GC時(shí)再清理掉。這一部分垃圾就稱為"浮動(dòng)垃圾"。

3.空間碎片:CMS是一款基于標(biāo)記-清除算法實(shí)現(xiàn)的收集器,所有會(huì)有空間碎片的現(xiàn)象,當(dāng)空間碎片過多時(shí),將會(huì)給大對(duì)象分配帶來很大麻煩,往往會(huì)出現(xiàn)老年代還有很大空間剩余,但是

無法找到足夠大的連續(xù)空間來分配當(dāng)前對(duì)象,不得不提前觸發(fā)一次Full GC。

G1收集器

G1收集器是當(dāng)今收集器技術(shù)發(fā)展的最前沿成果之一,G1是一款面向服務(wù)端應(yīng)用的垃圾收集器,HotSpot開發(fā)團(tuán)隊(duì)賦予它的使命是未來可以替換掉JDK1.5中發(fā)布的CMS收集器。在G1之前的其他收集器進(jìn)行收集的范圍都是整個(gè)新生代或者老年代,而G1不再是這樣。使用G1收集器時(shí),Java堆的內(nèi)存布局就與其他收集器有很大差別,它將整個(gè)Java堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續(xù))的集合。與其他GC收集器相比,G1具備如下特點(diǎn):

(1)并行與并發(fā):G1能充分利用多CPU,多核環(huán)境下的硬件優(yōu)勢(shì),使用多個(gè)CPU(CPU或者CPU核心)來縮短Stop-The-World停頓時(shí)間,部分其他收集器原本需要停頓Java線程執(zhí)行的GC動(dòng)作,G1收集器仍然可以通過并發(fā)的方式讓Java程序繼續(xù)執(zhí)行。

(2)分代收集:與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能獨(dú)立管理整個(gè)GC堆,但它能夠采用不同的方式去處理新創(chuàng)建的對(duì)象和已經(jīng)存活了一段時(shí)間、熬過多次GC的舊對(duì)象以獲取更好的收集效果。

(3)空間整合:與CMS的“標(biāo)記-清理”算法不同,G1從整體來看是基于“標(biāo)記-整理”算法實(shí)現(xiàn)的收集器,從局部(兩個(gè)Region之間)上來看是基于“復(fù)制”算法實(shí)現(xiàn)的,但無論如何,這兩種算法都意味著G1運(yùn)作期間不會(huì)產(chǎn)生內(nèi)存空間碎片,收集后能提供規(guī)整的可用內(nèi)存。這種特性有利于程序長(zhǎng)時(shí)間運(yùn)行,分配大對(duì)象時(shí)不會(huì)因?yàn)闊o法找到連續(xù)內(nèi)存空間而提前觸發(fā)下一次GC。

(4)可預(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í)間不得超過N毫秒,這幾乎已經(jīng)是實(shí)時(shí)Java的垃圾收集器的特征了。

G1收集器之所以能建立可預(yù)測(cè)的停頓時(shí)間模型,是因?yàn)樗梢杂杏?jì)劃地避免在整個(gè)Java堆中進(jìn)行全區(qū)域的垃圾收集。G1跟蹤各個(gè)Region里面的垃圾堆積的價(jià)值大小(回收所獲得的空間大小以及回收所需時(shí)間的經(jīng)驗(yàn)值),在后臺(tái)維護(hù)一個(gè)優(yōu)先列表,每次根據(jù)允許的收集時(shí)間,優(yōu)先回收價(jià)值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃分內(nèi)存空間以及有優(yōu)先級(jí)的區(qū)域回收方式,保證了G1收集器在有限的時(shí)間內(nèi)可以獲取盡可能高的收集效率。

在G1收集器中,Region之間的對(duì)象引用以及其他收集器中的新生代與老年代之間的對(duì)象引用,虛擬機(jī)都是使用Remembered Set來避免全堆掃描的。G1中每個(gè)Region都有一個(gè)與之對(duì)應(yīng)的Remembered Set,虛擬機(jī)發(fā)現(xiàn)程序在對(duì)Reference類型的數(shù)據(jù)進(jìn)行寫操作時(shí),會(huì)產(chǎn)生一個(gè)Write Barrier暫時(shí)中斷寫操作,檢查Reference引用的對(duì)象是否處于不同的Region之中(在分代的例子中就是檢查是否老年代中的對(duì)象引用了新生代中的對(duì)象),如果是,便通過CardTable把相關(guān)引用信息記錄到被引用對(duì)象所屬的Region的Remembered Set之中。當(dāng)進(jìn)行內(nèi)存回收時(shí),在GC根節(jié)點(diǎn)的枚舉范圍中加入Remembered Set即可保證不對(duì)全堆掃描也不會(huì)有遺漏。

如果不計(jì)算維護(hù)Remembered Set的操作,G1收集器的運(yùn)作大致可劃分為以下幾個(gè)步驟:

初始標(biāo)記(Initial Marking)

并發(fā)標(biāo)記(Concurrent Marking)

最終標(biāo)記(Final Marking)

篩選回收(Live Data Counting and Evacuation)

對(duì)CMS收集器運(yùn)作過程熟悉的讀者,一定已經(jīng)發(fā)現(xiàn)G1的前幾個(gè)步驟的運(yùn)作過程和CMS有很多相似之處。初始標(biāo)記階段僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,并且修改TAMS的值,讓下一階段用戶程序并發(fā)運(yùn)行時(shí),能在正確可用的Region中創(chuàng)建新對(duì)象,這階段需要停頓線程,但耗時(shí)很短。并發(fā)標(biāo)記階段是從GC Root開始對(duì)堆中對(duì)象進(jìn)行可達(dá)性分析,找出存活的對(duì)象,這階段耗時(shí)較長(zhǎng),但可與用戶程序并發(fā)執(zhí)行。而最終標(biāo)記階段則是為了修正在并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分標(biāo)記記錄,虛擬機(jī)將這段時(shí)間對(duì)象變化記錄在線程Remembered Set Logs里面,最終標(biāo)記階段需要把Remmbered Set Logs的數(shù)據(jù)合并到Remembered Set中,這階段需要停頓線程,但是可并行執(zhí)行。最后篩選回收階段首先對(duì)各個(gè)Region的回收價(jià)值和成本進(jìn)行排序,根據(jù)用戶所期望的GC停頓時(shí)間來制定回收計(jì)劃。

關(guān)于垃圾收集器暫時(shí)就了解這么多內(nèi)容,關(guān)于可用的組合圖3-5一目了然。

那么如何閱讀GC日志呢?

想要閱讀GC日志,首先必須要開啟GC日志,開啟方式如下圖:

[java]view plaincopy

packagecom.general.garbage;

publicclassTestGCLog?{

privateObject[]?obj;

publicstaticvoidmain(String[]?args)?{

TestGCLog?gcLog=newTestGCLog();

gcLog.createObjects();

gcLog.clear();

System.gc();

}

publicvoidcreateObjects(){

obj=newObject[]{newbyte[1024*1024],newbyte[1024*1024]};

}

publicvoidclear(){

obj=null;

}

}

上面代碼運(yùn)行之后,打印出的相關(guān)的GC信息如下:

[java]view plaincopy

0.104:?[GC?(System.gc())?[PSYoungGen:?3922K->616K(36352K)]?3922K->624K(119808K),0.0035976secs]?[Times:?user=0.00sys=0.00,?real=0.00secs]

0.108:?[Full?GC?(System.gc())?[PSYoungGen:?616K->0K(36352K)]?[ParOldGen:?8K->519K(83456K)]?624K->519K(119808K),?[Metaspace:?2574K->2574K(1056768K)],0.0061018secs]?[Times:?user=0.00sys=0.00,?real=0.01secs]

Heap

PSYoungGen??????total?36352K,?used?312K?[0x00000000d7980000,0x00000000da200000,0x0000000100000000)

eden?space?31232K,1%?used?[0x00000000d7980000,0x00000000d79ce2b8,0x00000000d9800000)

from?space?5120K,0%?used?[0x00000000d9800000,0x00000000d9800000,0x00000000d9d00000)

to???space?5120K,0%?used?[0x00000000d9d00000,0x00000000d9d00000,0x00000000da200000)

ParOldGen???????total?83456K,?used?519K?[0x0000000086c00000,0x000000008bd80000,0x00000000d7980000)

object?space?83456K,0%?used?[0x0000000086c00000,0x0000000086c81ef0,0x000000008bd80000)

Metaspace???????used?2581K,?capacity?4486K,?committed?4864K,?reserved?1056768K

classspace????used?285K,?capacity?386K,?committed?512K,?reserved?1048576K

下面我們看看如何理解這段GC日志,

[GC (System.gc()) [PSYoungGen: 3922K->616K(36352K)] 3922K->624K(119808K), 0.0035976 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

[Full GC (System.gc()) [PSYoungGen: 616K->0K(36352K)] [ParOldGen: 8K->519K(83456K)] 624K->519K(119808K), [Metaspace: 2574K->2574K(1056768K)], 0.0061018 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

GC和Full GC表示垃圾回收的停頓類型

PSYoungGen:表示新生代使用的是Parallel Scavenge垃圾收集器

3922K(gc前新生代所用內(nèi)存)->616K(gc后新生代所用內(nèi)存)(36352K(新生代總內(nèi)存))

3922K(gc前Java堆已使用內(nèi)存)->624K(gc后Java堆已使用內(nèi)存)(119808K(Java堆總?cè)萘?)

0.0035976 secs:表示GC所占用時(shí)間。

下面看下兩幅圖,就對(duì)GC日志一目了然了。

上面兩幅圖來源于:http://blog.csdn.net/wanglha/article/details/48713217

內(nèi)存如何分配的?

對(duì)象的內(nèi)存分配,往大方向講,就是在堆上分配(但也可能經(jīng)過JIT編譯后被拆散為標(biāo)量類型并間接地棧上分配,這點(diǎn)屬于JVM編譯優(yōu)化的操作,這個(gè)會(huì)在后續(xù)的博客中說明),對(duì)象主要分配在新生代的Eden區(qū)上,如果啟動(dòng)了本地線程分配緩沖,將按線程優(yōu)先在TLAB上分配。少數(shù)情況下也可能會(huì)直接分配在老年代中,分配的規(guī)則并不是百分之百固定的,其細(xì)節(jié)取決于當(dāng)前使用的是哪一種垃圾收集器組合,還有虛擬機(jī)中與內(nèi)存相關(guān)的參數(shù)的設(shè)置。

對(duì)象優(yōu)先在Eden分配:大多數(shù)情況下,對(duì)象在新生代Eden區(qū)中分配。當(dāng)Eden區(qū)沒有足夠空間進(jìn)行分配時(shí),虛擬機(jī)將發(fā)起一次Minor GC。

[java]view plaincopy

privatestaticfinalint_1MB=1024*1024;

/*-XX:+PrintGCTimeStamps?-XX:+PrintGCDetails???-Xms20M?-Xmx20M?-Xmn7M?-XX:SurvivorRatio=8*/

publicstaticvoidtestAllocation(){

byte[]?allocation1,allocation2,allocation3,allocation4;

allocation1=newbyte[2*_1MB];

allocation2=newbyte[2*_1MB];

allocation3=newbyte[2*_1MB];//發(fā)生一次Minor?GC

allocation4=newbyte[4*_1MB];//最后在allocation3在新生代里,其余4個(gè)對(duì)象在老年代里

}

0.111:?[GC?(Allocation?Failure)?[PSYoungGen:?4937K->504K(6656K)]?4937K->4672K(19968K),0.0041380secs]?[Times:?user=0.05sys=0.00,?real=0.00secs]

Heap

PSYoungGen??????total?6656K,?used?2613K?[0x00000000ff900000,0x0000000100000000,0x0000000100000000)

eden?space?6144K,34%?used?[0x00000000ff900000,0x00000000ffb0f748,0x00000000fff00000)

from?space?512K,98%?used?[0x00000000fff00000,0x00000000fff7e010,0x00000000fff80000)

to???space?512K,0%?used?[0x00000000fff80000,0x00000000fff80000,0x0000000100000000)

ParOldGen???????total?13312K,?used?8264K?[0x00000000fec00000,0x00000000ff900000,0x00000000ff900000)

object?space?13312K,62%?used?[0x00000000fec00000,0x00000000ff412030,0x00000000ff900000)

Metaspace???????used?2581K,?capacity?4486K,?committed?4864K,?reserved?1056768K

classspace????used?285K,?capacity?386K,?committed?512K,?reserved?1048576K

//當(dāng)注釋allocation3和4的這兩句代碼時(shí),GC的運(yùn)行日志如下:所以,這樣就很好理解了上文中為何會(huì)在allocation3處發(fā)生了一次Minor?GC。

Heap

PSYoungGen??????total?6656K,?used?5061K?[0x00000000ff900000,0x0000000100000000,0x0000000100000000)

eden?space?6144K,82%?used?[0x00000000ff900000,0x00000000ffdf14d0,0x00000000fff00000)

from?space?512K,0%?used?[0x00000000fff80000,0x00000000fff80000,0x0000000100000000)

to???space?512K,0%?used?[0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)

ParOldGen???????total?13312K,?used?0K?[0x00000000fec00000,0x00000000ff900000,0x00000000ff900000)

object?space?13312K,0%?used?[0x00000000fec00000,0x00000000fec00000,0x00000000ff900000)

Metaspace???????used?2580K,?capacity?4486K,?committed?4864K,?reserved?1056768K

classspace????used?285K,?capacity?386K,?committed?512K,?reserved?1048576K

上述代碼運(yùn)行在新生代總空間為7M的虛擬機(jī)里,當(dāng)分配到第三個(gè)對(duì)象時(shí)就發(fā)生了一次GC,相信大家對(duì)比上面的兩個(gè)GC日志就明白了為什么在會(huì)allocation3=new byte[2*_1MB]處發(fā)生GC。

大對(duì)象直接進(jìn)入老年代:虛擬機(jī)提供了一個(gè)-XX:PretenureSizeThreshold參數(shù),令大于這個(gè)設(shè)置值的對(duì)象直接在老年代分配。這樣做的目的是避免在Eden區(qū)及兩個(gè)Survivor區(qū)之間發(fā)生大量的內(nèi)存復(fù)制。

長(zhǎng)期存活的對(duì)象將進(jìn)入老年代:既然虛擬機(jī)采用了分代收集的思想來管理內(nèi)存,那么內(nèi)存回收時(shí)就必須能識(shí)別哪些對(duì)象應(yīng)放在新生代,哪些對(duì)象應(yīng)放在老年代中。為了做到這點(diǎn),虛擬機(jī)給每個(gè)對(duì)象定義了一個(gè)對(duì)象年齡計(jì)數(shù)器。如果對(duì)象在Eden出生并經(jīng)過第一次Minor GC后仍然存活,并且能被Survivor容納的話,將被移動(dòng)到Survivor空間中,并且對(duì)象年齡設(shè)為1。對(duì)象在Survivor區(qū)中每“熬過”一次Minor GC,年齡就增加1歲,當(dāng)它的年齡增加到一定程度(默認(rèn)為15歲),就將會(huì)被晉升到老年代中。對(duì)象晉升老年代的年齡閾值,可以通過參數(shù)-XX:MaxTenuringThreshold設(shè)置。

動(dòng)態(tài)對(duì)象年齡判定:為了能更好地適應(yīng)不同程序的內(nèi)存狀況,虛擬機(jī)并不是永遠(yuǎn)地要求對(duì)象的年齡必須達(dá)到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對(duì)象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對(duì)象就可以直接進(jìn)入老年代,無須等到MaxTenuringThreshold中要求的年齡。

空間分配擔(dān)保:在發(fā)生Minor GC之前,虛擬機(jī)會(huì)先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對(duì)象總空間,如果這個(gè)條件成立,那么Minor GC可以確保是安全的。如果不成立,則虛擬機(jī)會(huì)查看HandlePromotionFailure設(shè)置值是否允許擔(dān)保失敗。如果允許,那么會(huì)繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對(duì)象的平均大小,如果大于,將嘗試著進(jìn)行一次Minor GC,盡管這次Minor GC是有風(fēng)險(xiǎn)的;如果小于,或者HandlePromotionFailure設(shè)置不允許冒險(xiǎn),那這時(shí)也要改為進(jìn)行一次Full GC。

這里有必要說一下Minor GC和Full GC:

新生代GC(Minor GC):指發(fā)生在新生代的垃圾收集動(dòng)作,因?yàn)镴ava對(duì)象大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。

老年代GC(Major GC/Full GC):指發(fā)生在老年代的GC,出現(xiàn)了Major GC,經(jīng)常會(huì)伴隨至少一次的Minor GC(但非絕對(duì)的,在Parallel Scavenge收集器的收集策略里就有直接進(jìn)行Major GC的策略選擇過程),Major GC的速度一般會(huì)比Minor GC 慢10倍以上。

其實(shí)到了這里,第三章的內(nèi)容差不多已經(jīng)總結(jié)完全了,后面會(huì)繼續(xù)往下總結(jié),等把相關(guān)章節(jié)的內(nèi)容整理完畢,再來對(duì)這個(gè)系列的文章進(jìn)行review進(jìn)而修改,如果覺得不錯(cuò),請(qǐng)頂一下哈。

轉(zhuǎn)載請(qǐng)注明出處:http://blog.csdn.net/android_jiangjun/article/details/78125281

作者:GeneralAndroid

鏈接:www.lxweimin.com/p/bee8e30c8aea?

來源:簡(jiǎn)書

著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容