[toc]
前言
說(shuō)起垃圾收集GC(Garbage Collection),大家都不會(huì)陌生,他是JVM實(shí)現(xiàn)里非常重要的一環(huán),JVM成熟的內(nèi)存動(dòng)態(tài)分配與回收技術(shù)使Java(當(dāng)然還有運(yùn)行在JVM上的語(yǔ)言,如Scala等)程序員在提升開(kāi)發(fā)效率上獲得了驚人的便利。理解GC,對(duì)于理解JVM和Java語(yǔ)言有著非常重要的作用,并且當(dāng)我們需要排查各種內(nèi)存溢出、內(nèi)存泄漏問(wèn)題時(shí),當(dāng)垃圾收集成為系統(tǒng)達(dá)到更高并發(fā)量的瓶頸時(shí),只有深入理解GC和內(nèi)存分配,才能對(duì)這些自動(dòng)化的技術(shù)實(shí)施必要的監(jiān)控和條件。
在Java運(yùn)行時(shí)數(shù)據(jù)區(qū)中,程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧三個(gè)區(qū)域都是線程私有的,隨線程而生,隨線程而滅。在方法結(jié)束或線程結(jié)束時(shí),內(nèi)存自然就跟著回收了,不需要過(guò)多考慮回收的問(wèn)題。而Java堆和方法區(qū)不一樣一個(gè)接口中多個(gè)實(shí)現(xiàn)類需要內(nèi)存可能不一樣,一個(gè)方法中多個(gè)分支需要的內(nèi)存也可能不一樣,我們只有在程序處于運(yùn)行期間才能知道創(chuàng)建那些對(duì)象,這部分分內(nèi)存的分配和回收都是動(dòng)態(tài)的,垃圾收集器關(guān)注的是這部分內(nèi)存,后序討論的內(nèi)存分配回收也是指的這一塊,尤其需要注意。
GC主要回答了以下三個(gè)問(wèn)題:
- 哪些內(nèi)存需要回收?
- 什么時(shí)候回收?
- 如何回收?
對(duì)象存活判定算法
在堆里存放著Java世界中幾乎所有的對(duì)象實(shí)例,垃圾收集器在對(duì)堆進(jìn)行回收前,首要的就是確定這些對(duì)象中哪些存活著,哪些已經(jīng)死去了(即不可能再背任何途徑使用的對(duì)象)
引用計(jì)數(shù)算法
引用計(jì)數(shù)算法是在JVM中被摒棄的一種對(duì)象判定算法,不過(guò)他也有一些知名的應(yīng)用場(chǎng)景(如Python、FlashPlayer),因此在這里也簡(jiǎn)單介紹一下。
用引用計(jì)數(shù)器判斷對(duì)象是否存活的過(guò)程這樣的:給對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用他時(shí),計(jì)數(shù)器加1,當(dāng)應(yīng)用失效時(shí),計(jì)數(shù)器減1,任何時(shí)刻計(jì)數(shù)器為0的對(duì)象就是不可能再背使用的。
引用計(jì)數(shù)算法實(shí)現(xiàn)很簡(jiǎn)單,判定效率也很高,大部分情況是一個(gè)不錯(cuò)的算法。他沒(méi)有被JVM采用的原因是很難解決對(duì)象之間的循環(huán)引用問(wèn)題。例如下面的例子:
public class ReferenceCountingGc{
public Object instance=null;
private static final int _1MB=1024*1024;
private byte[] bigSize=new byte[2*_1MB];
public static void testGc(){
ReferenceCountingGc objA=new ReferenceCountingGc();
ReferenceCountingGc objB=new ReferenceCountingGc();
objA.instance=objB;
objB.instance=objA;
objA=null;
objB=null;
System.gc();
}
}
上面的這段代碼中,對(duì)象objA和對(duì)象objB都有相同字段instance,賦值objA.instance=objB;objB.instance=objA;,出此之外,這兩個(gè)對(duì)象再無(wú)引用。如果JVM采用引用計(jì)數(shù)算法來(lái)管理內(nèi)存,這兩個(gè)對(duì)象不可能被訪問(wèn),但是他們互相引用對(duì)方,導(dǎo)致他們引用計(jì)數(shù)不為0,所以引用計(jì)數(shù)器無(wú)法通知GC收集器回收他們。
而事實(shí)上執(zhí)行這段代碼,objA和objB是可以被回收的,下面介紹JVM實(shí)際使用的存活判定算法。
可達(dá)性分析算法
在主流商用程序語(yǔ)言實(shí)現(xiàn)中,都是通過(guò)可達(dá)性分析(tracing GC)來(lái)判定對(duì)象是否存活。此算法的基本思路是:通過(guò)一系列稱為GC Roots的對(duì)象作為起點(diǎn),從這些節(jié)點(diǎn)向下搜索,搜索所走過(guò)的路徑稱為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連(用圖論的話來(lái)說(shuō),就是GC Roots到這個(gè)對(duì)象不可達(dá))時(shí),則證明此對(duì)象是不可用的。用下圖來(lái)加以說(shuō)明:
上圖中,對(duì)象Object5、Object6、Object7雖然互有關(guān)聯(lián),但是他們到GC Roots是不可達(dá)的,所以他們將會(huì)判定為是可回收的對(duì)象。
可以看到,GC Roots在對(duì)象圖之外,是特別定義的起點(diǎn),不可能被對(duì)象圖內(nèi)的對(duì)象所引用。
準(zhǔn)確的說(shuō),GC Roots其實(shí)不是一組對(duì)象,而通常是一組特別管理的指向引用類型的對(duì)象的指針這些指針是tracing GC的trace的起點(diǎn)。他們不是對(duì)象圖中的對(duì)象,對(duì)象也不可能引用到這些外部的指針,這也是tracing GC算法不會(huì)出現(xiàn)循環(huán)引用的問(wèn)題的基本保證。因此也容易得出,只有引用類型的變量才被認(rèn)為是Roots,值類型的變量用于不被認(rèn)為是Roots,只用深刻理解引用類型和值類型的內(nèi)存分配和管理的不同,才能知道為什么Root只能是引用類型。
在Java中,可作為GC Roots的對(duì)象包括以下幾種:
- 虛擬機(jī)棧(棧幀中局部變量表,Local Variable Table)中的引用對(duì)象
- 方法區(qū)中類靜態(tài)屬性引用對(duì)象
- 方法區(qū)中常量引用對(duì)象
- 本地方法棧中JNI(即一般說(shuō)的Native方法)引用對(duì)象
- java虛擬機(jī)內(nèi)部引用,如基本數(shù)據(jù)類型對(duì)應(yīng)的Class對(duì)象,一些常駐的異常對(duì)象( NPE、OOM異常對(duì)象)等,還有系統(tǒng)類加載器
- 被同步鎖(synchronized關(guān)鍵字)持有的對(duì)象
- 反映java虛擬機(jī)內(nèi)部情況的JMXBean、JVMTI中注冊(cè)的回調(diào),本地代碼緩存等
選擇這些對(duì)象的依據(jù)是什么?
可以概括得出,可以作為GC Roots的節(jié)點(diǎn)主要在全局性的引用與執(zhí)行上下文中。要明確的是tracing gc必須以當(dāng)前存活的對(duì)象集為Roots,因此次必須選取確定的存活引用類型對(duì)象。GC管理區(qū)域是Java堆,虛擬機(jī)棧、方法區(qū)和本地方法棧不被GC所管理,因此選用這些區(qū)域內(nèi)引用的對(duì)象作為GC Roots,是不會(huì)被回收的。其中虛擬機(jī)棧和本地方法棧都是線程私有的內(nèi)存區(qū)域,只要線程沒(méi)有終止,就能確保他們中引用的對(duì)象的存活。而方法區(qū)中類靜態(tài)屬性引用的對(duì)象顯然存活的。常量引用的對(duì)象當(dāng)前可能存活,因此也可能是GC Roots的一部分。
兩次標(biāo)記與finalize()
即使在可達(dá)性分析算法中不可達(dá)的對(duì)象,也不是一定會(huì)死亡的,他們暫時(shí)都處于緩刑階段,要真正宣告死亡,至少經(jīng)歷兩次標(biāo)記過(guò)程:
- 如果對(duì)象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒(méi)用與GC Roots相連接的引用鏈,那么他被第一次標(biāo)記并且進(jìn)行一次篩選,篩選條件是此對(duì)象是否有必要執(zhí)行finalize()方法。當(dāng)對(duì)象沒(méi)用覆蓋
finalize()方法或者finalize()方法以及被虛擬機(jī)調(diào)用過(guò),虛擬機(jī)將這兩種情況視為“沒(méi)必要執(zhí)行”。 - 如果這個(gè)對(duì)象判定有必要執(zhí)行
finalize()方法,那么將此對(duì)象放置在一個(gè)叫做F-Queue的隊(duì)列中,并在稍后右一個(gè)虛擬機(jī)自動(dòng)建立的、低優(yōu)先級(jí)的Finalizer線程執(zhí)行他。這里所謂的“執(zhí)行”是虛擬機(jī)會(huì)觸發(fā)此方法,但并不承諾會(huì)等待他運(yùn)行結(jié)束,原因是:如果一個(gè)對(duì)象在finalize()方法中執(zhí)行緩慢,或者發(fā)生了死循環(huán)(更極端的情況),將很可能導(dǎo)致F-Queue隊(duì)列中的其他對(duì)象永久處于等待,甚至導(dǎo)致整個(gè)內(nèi)存回收系統(tǒng)崩潰。
finalize()方法是對(duì)象逃脫死亡命運(yùn)的最后一次機(jī)會(huì),稍后GC將對(duì)F-Queue隊(duì)列中的對(duì)象進(jìn)行第二次小規(guī)模的標(biāo)記。如果對(duì)象想在finalize()方法中拯救自己,只要重新與引用鏈上的任何一個(gè)對(duì)象建立關(guān)聯(lián)即可,例如把自己(this關(guān)鍵字)賦值給某個(gè)變量或者對(duì)象的成員變量,這樣在第二次標(biāo)記時(shí)它被移移出即將回收的集合;如果對(duì)象這時(shí)候還沒(méi)有逃脫,基本上他就真的被回收了。
值的注意的是,如果代碼中有兩段一模一樣的代碼,執(zhí)行結(jié)果卻是一次逃脫成功,一次失敗。這是因?yàn)槿魏螌?duì)象的finalize()方法都只會(huì)被系統(tǒng)調(diào)用一次,如果對(duì)象面臨下一次回收,他的finalize()方法不會(huì)再被執(zhí)行,因此第二次逃脫失敗。
需要說(shuō)明的是,使用finalize()方法來(lái)拯救對(duì)象是不值得提成的,因?yàn)樗皇荂/C++中的析構(gòu)函數(shù),而是Java剛誕生時(shí)為了使C/C++程序員更容易接受他所做的妥協(xié)。他運(yùn)行代價(jià)高昂,不確定性大,無(wú)法保證各個(gè)對(duì)象的調(diào)用順序。finalize()能做的工作,使用try-finally或者其他方法都更適合、及時(shí),所以建議大家忘掉這個(gè)方法。
回收方法區(qū)
很多人認(rèn)為方法區(qū)沒(méi)有垃圾回收,Java虛擬機(jī)規(guī)范中確實(shí)說(shuō)過(guò)不要求,而且方法區(qū)中進(jìn)行垃圾收集的性價(jià)比較低,在堆中,尤其是新生代,常規(guī)應(yīng)用進(jìn)行一次垃圾收集可以回收70~95空間,而方法區(qū)的效率遠(yuǎn)低于此,在JDK1.8中,JVM摒棄了永久代,用元空間來(lái)作為方法區(qū)的實(shí)現(xiàn),下面介紹的將是元空間的垃圾回收。
元空間的內(nèi)存管理是由元空間虛擬機(jī)來(lái)完成的。先前,對(duì)于類的元數(shù)據(jù)我們需要不同的垃圾回收器進(jìn)行處理,現(xiàn)在之需要執(zhí)行元空間虛擬機(jī)的C++代碼即可完成。在元空間中,類和其元數(shù)據(jù)的生命周期和其對(duì)應(yīng)的類加載器相同的。換句話說(shuō),只要類的加載器存活,其加載的類的元數(shù)據(jù)也是存活的,因而不會(huì)被回收掉。
準(zhǔn)確的來(lái)說(shuō),每一個(gè)類加載器的存儲(chǔ)區(qū)都稱作一個(gè)元空間,所有的元空間和加在一起就是我們所說(shuō)的元空間。當(dāng)一個(gè)類加載器被垃圾收集器標(biāo)記為不在存活,對(duì)應(yīng)的元空間會(huì)被回收。在元空間的回收過(guò)程中沒(méi)有重定位和壓縮等功能,但是元空間內(nèi)的元數(shù)據(jù)會(huì)進(jìn)行掃描來(lái)確定Java引用。
垃圾收集算法
標(biāo)記-清除(Mark-Sweep)算法
標(biāo)記-清除(Mark-Sweep) 算法是最基礎(chǔ)的垃圾回收算法,后續(xù)的收集算法都是基于他的思路并對(duì)其不足進(jìn)行改進(jìn)的。顧名思義,算法分成標(biāo)記和清除兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對(duì)象。
標(biāo)記-清除算法的兩個(gè)不足主要有以下兩點(diǎn):
- 空間問(wèn)題,標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致以后在程序運(yùn)行過(guò)程中需要分配較大的對(duì)象時(shí),無(wú)法找到足夠的連續(xù)內(nèi)存而不得不觸發(fā)另一次垃圾收集動(dòng)作。
-
效率問(wèn)題,因?yàn)閮?nèi)存碎片的存在,操作會(huì)變得更加費(fèi)時(shí),因?yàn)椴檎蚁乱粋€(gè)可用空閑塊已不再是一個(gè)簡(jiǎn)單的操作。
標(biāo)記-清除算法的執(zhí)行過(guò)程如下圖所示:
image
復(fù)制(Copying)算法
復(fù)制(Copying)算法:為了解決標(biāo)記-清除算法的效率問(wèn)題,一種復(fù)制(Copying) 的收集算法出現(xiàn)了,思想是:將可用內(nèi)存按容量分成大小相等的兩塊,每次只使用其中一塊,當(dāng)這一塊內(nèi)存用完,就將還存活的對(duì)象復(fù)制到另一塊上面,然后再把已經(jīng)使用過(guò)的內(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)存縮小為原來(lái)的一半,代價(jià)可能過(guò)高了,復(fù)制算法的執(zhí)行過(guò)程如下圖所示:
Minor GC與復(fù)制算法
現(xiàn)在的商業(yè)虛擬機(jī)都使用的賦值算法來(lái)回收新生代。新生代的GC又叫做Minor GC,IBM公司專門研究表明:新生代的對(duì)象98%是朝生夕死的,所以Minor GC非常頻繁,一般回收速度比較快,同時(shí)朝生夕死也使得Minor GC使用復(fù)制算法不需要按照1:1的比例來(lái)劃分新生代內(nèi)存空間。
Minor GC過(guò)程
事實(shí)上,新生代內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間(From Survivor和To Survivor),每次Minor GC都是用Eden和From Survivor,當(dāng)回收時(shí),將Eden和From Survivor中還存活的對(duì)象一次性復(fù)制到另外一塊To Survivor空間上,最后清理掉Eden和剛才使用的Survivor空間,一次Minor GC結(jié)束的時(shí)候,Eden空間和From Survivor空間都是空的,而To Survivor空間里面的存儲(chǔ)著存活的對(duì)象。在下次Minor GC的時(shí)候兩個(gè)Survivor空間交換他們的標(biāo)簽,現(xiàn)在空的From Survivor標(biāo)記為To Survivor,To Survivor標(biāo)記為From。因此在Minor GC結(jié)束的時(shí)候Eden空間是空的,兩個(gè)Survivor空間中一個(gè)是空的,而另一個(gè)存儲(chǔ)著存活的對(duì)象。
HotSpot虛擬機(jī)默認(rèn)Eden:Survivor的比例是8:1:1,所以每次新生代中內(nèi)存空間為整個(gè)新生代容量的90%(80%+10%),只要10%的容量會(huì)被浪費(fèi)掉。
分配擔(dān)保
上面說(shuō)的98%的對(duì)象可回收只是一般場(chǎng)景下的數(shù)據(jù),我們沒(méi)有辦法保證每次回收只有不多于10%的對(duì)象存活,當(dāng)Survivor空間不夠用時(shí),需要依賴老年代內(nèi)存進(jìn)行分配擔(dān)保(Handle Promotion)。如果另一塊Survivor上沒(méi)有足夠的空間存放上一次新生代收集下來(lái)的存活對(duì)象,這些對(duì)象將直接通過(guò)分配擔(dān)保進(jìn)入老年代。
標(biāo)記-整理(Mark-Compact)算法
賦值算法在對(duì)象存活率較高的時(shí)要進(jìn)行較多的復(fù)制操作,效率將會(huì)變低,更關(guān)鍵的是:如果不想浪費(fèi)50%的空間,就需要有額外的空間進(jìn)行分配擔(dān)保,以應(yīng)對(duì)被使用的內(nèi)存中所有對(duì)象都都100%存活的極端情況,所以在老年代一般不能直接選用復(fù)制算法。
根據(jù)老年代的特點(diǎn),標(biāo)記-整理(Mark-Compact)算法被提出來(lái),主要思想為:此算法的標(biāo)記過(guò)程與標(biāo)記-清除算法一樣,但后續(xù)步驟不是直接對(duì)可回收的對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉邊界以外的內(nèi)存。具體示意圖如下所示:
分代收集(Generational Collection)算法
當(dāng)前商業(yè)的虛擬機(jī)的垃圾收集都是采用分代收集算法(Generation Collection)算法,此算法相較于前幾種沒(méi)有什么新的特征,主要思想為:根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊,一般是把Java堆分為新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適合的收集算法:
- 新生代在新生代中,每次垃圾收集時(shí)都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,那就采用復(fù)制算法,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集。
- 老年代在老年代中,因?yàn)閷?duì)象存活率高,沒(méi)有額外的空間對(duì)他進(jìn)行內(nèi)存擔(dān)保,就必須使用標(biāo)記-清除或標(biāo)記-整理算法來(lái)進(jìn)行回收。
HotSpot的算法實(shí)現(xiàn)
上面主要從理論上介紹對(duì)象存活判定算法和垃圾收集算法,而在HotSpot虛擬機(jī)上實(shí)現(xiàn)這些算法,必須對(duì)算法的執(zhí)行效率有嚴(yán)格的考量,才能保證虛擬機(jī)高效運(yùn)行。
枚舉根節(jié)點(diǎn)
從可達(dá)性分析中從GC Roots節(jié)點(diǎn)找引用鏈這個(gè)操作為例,可作為GC Roots的節(jié)點(diǎn)主要在全局性引用(例如常量或類靜態(tài)屬性)與執(zhí)行上下文(例如棧幀中的局部變量表),現(xiàn)在很多應(yīng)用僅僅方法區(qū)就有數(shù)百兆,如果逐個(gè)檢查這里面的引用,那么必然會(huì)消耗很多時(shí)間。
GC停頓(Stop The Word)
另外,可達(dá)性分析工作必須在一個(gè)確保一致性的快照中進(jìn)行,這里的一致性的意思是指在整個(gè)分析期間整個(gè)執(zhí)行系統(tǒng)看起來(lái)就像被凍結(jié)在某個(gè)時(shí)間上,不可用出現(xiàn)分析過(guò)程中對(duì)象引用關(guān)系還在不斷變化的情況,這是保證分析結(jié)果準(zhǔn)確性的基礎(chǔ)好。這點(diǎn)是導(dǎo)致GC進(jìn)行時(shí)必時(shí)停頓所有java執(zhí)行線程(稱為Stop The World) 的其中一個(gè)重要原因,即使是在號(hào)稱(幾乎)不會(huì)發(fā)生停頓的CMS收集器,枚舉根節(jié)點(diǎn)時(shí)也是必須要停頓的。
準(zhǔn)確式GC與OopMap
由于目前的主流的Java虛擬機(jī)使用的都是準(zhǔn)確式GC(即使用準(zhǔn)確式內(nèi)存管理,虛擬機(jī)可以知道內(nèi)存中某個(gè)位置的數(shù)據(jù)具體的類型),所以當(dāng)執(zhí)行系統(tǒng)停頓下來(lái)后,并不需要一個(gè)不停的檢查完所有的執(zhí)行上下文和全局引用位置,虛擬機(jī)應(yīng)當(dāng)是有辦法直接得知那些地方存放著對(duì)象引用,在HotSoot的實(shí)現(xiàn)中,是使用一組稱為OopMap的數(shù)據(jù)結(jié)構(gòu)來(lái)達(dá)到這個(gè)目的的,在類加載完成的時(shí)候,HotSpot就把對(duì)象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計(jì)算出來(lái),在JIT編譯過(guò)程中,也會(huì)在特定位置記錄下棧和寄存器中哪些位置是引用。這樣GC在掃描就可以直接得知這些消息了。
安全點(diǎn)(Safeponit)-進(jìn)行GC時(shí)程序停頓的位置
在OopMap的協(xié)助下,HotSpot可以快速且準(zhǔn)確的完成GC Roots枚舉,但一個(gè)很現(xiàn)實(shí)的問(wèn)題隨之而來(lái):可能導(dǎo)致引用關(guān)系變化,或者說(shuō)OopMap內(nèi)容變化的指令非常多,如果為每一條指令都生成對(duì)應(yīng)的OopMap,那么需要大量的額外空間,這樣GC的空間成本變的很高。
為此,HotSpot選擇不為每條指令都生成OopMap,而是在指定位置記錄這些信息,這些位置稱為安全點(diǎn)(Safeponit).也就是說(shuō),程序執(zhí)行時(shí)并非所有地方都能停頓下來(lái)開(kāi)始GC,只有到達(dá)安全點(diǎn)時(shí)才能暫停。Safeponit的選定既不能太少以至于讓GC等待時(shí)間太長(zhǎng),也不能過(guò)于頻繁以至于過(guò)分增大運(yùn)行時(shí)的負(fù)荷。所以,安全點(diǎn)的選定基本上是以程序是否具有讓程序執(zhí)行的特征為標(biāo)志進(jìn)行選定的,因?yàn)槊恳粭l指令的時(shí)間非常短暫,程序不太可能因?yàn)橹噶盍鏖L(zhǎng)度太長(zhǎng)這個(gè)原因而過(guò)長(zhǎng)時(shí)間運(yùn)行,長(zhǎng)時(shí)間運(yùn)行的最明顯的特征就是指令序列復(fù)用,例如方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等,所以具有這些功能的指令才會(huì)產(chǎn)生SafePonit。
對(duì)于Safeponit,另一個(gè)需要考慮的問(wèn)題是如何在GC發(fā)生時(shí)讓所有線程(這里不包括執(zhí)行JNI調(diào)用的線程)都跑到最近的安全點(diǎn)上在停頓下來(lái)這里有兩種方案可供選擇:
- 搶先式中斷(Preemptive Suspension): 搶先式中斷不需要線程的執(zhí)行代碼主動(dòng)去配合,在GC發(fā)生時(shí),首先把所有線程全部中斷,如果發(fā)現(xiàn)有線程中斷的地方不在安全點(diǎn)上,就恢復(fù)線程,讓他跑到安全點(diǎn)上?,F(xiàn)在幾乎沒(méi)有虛擬機(jī)實(shí)現(xiàn)采用搶先式中斷來(lái)暫停線程從而響應(yīng)GC事件。
- 主動(dòng)式中斷(Volunntary Suspension):主動(dòng)式中斷的思想是當(dāng)GC需要中線程的時(shí)候,不直接對(duì)線程進(jìn)行操作,僅僅簡(jiǎn)單的設(shè)置一個(gè)標(biāo)志,各個(gè)線程執(zhí)行過(guò)程時(shí)主動(dòng)去輪詢這個(gè)標(biāo)志,發(fā)現(xiàn)中斷標(biāo)志位真時(shí)就自己中斷掛起。輪詢的標(biāo)志地方和安全點(diǎn)時(shí)重合的,另外在加上創(chuàng)建對(duì)象需要分配內(nèi)存的地方。
安全區(qū)域(Safe Region)
Safe Region機(jī)制保證了程序執(zhí)行時(shí),在不太長(zhǎng)的時(shí)間內(nèi)就會(huì)遇到可進(jìn)入GC的Safeponit。但是,程序不執(zhí)行的時(shí)候(如線程處于Sleep狀態(tài)或者Blocked狀態(tài)),這時(shí)線程無(wú)法響應(yīng)JVM的中斷請(qǐng)求,走到安全的地方中斷掛起,這時(shí)候就需要安全區(qū)域(Safe Region)來(lái)解決。
安全區(qū)域是指在一段代碼片段中,引用關(guān)系不會(huì)發(fā)生變化。在這個(gè)區(qū)域中的任意地方開(kāi)始GC都是安全的。我們也可以把Safe Region看成被擴(kuò)展的Safeponit。
在線程執(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)的線程了。在線程要離開(kāi)Safe Region時(shí),他要檢查系統(tǒng)是否已經(jīng)完成了根節(jié)點(diǎn)枚舉(或者是整個(gè)GC過(guò)程),如果完成了,那線程就繼續(xù)執(zhí)行,否則就必須等待直到收到可以安全離開(kāi)Safe Region的信號(hào)為止。
內(nèi)存分配策略
Java的自動(dòng)內(nèi)存管理最終可以歸結(jié)為自動(dòng)化的解決兩個(gè)問(wèn)題:
- 給對(duì)象分配內(nèi)存
-
回收分配對(duì)象的內(nèi)存
對(duì)象的內(nèi)存分配通常是在堆上分配(除此以外還有可能經(jīng)過(guò)JIT編譯后被拆散為標(biāo)量類型并間接的在棧上分配),對(duì)象主要分配在新生代Eden區(qū)上,如果啟動(dòng)了本地線程分配緩沖,將按線程優(yōu)先在TLAB上分配,少數(shù)情況下可能會(huì)直接分配在老年代,分配規(guī)則并不是固定的,時(shí)間取決于垃圾收集器的具體組合以及虛擬機(jī)中與內(nèi)存相關(guān)的參數(shù)配置。至于內(nèi)存回收策略,在上文以及描述的詳細(xì)了。
下面以使用Serial/Serial Old收集器為例,介紹內(nèi)存分配的策略。
對(duì)象優(yōu)先在Eden區(qū)分配
大多數(shù)情況下,對(duì)象在新生代Eden區(qū)中分配。當(dāng)Eden區(qū)沒(méi)有足夠空間進(jìn)行內(nèi)存分配時(shí),虛擬機(jī)將發(fā)起一次Minor GC。
大對(duì)象直接進(jìn)入老年代
所謂的大對(duì)象是指,需要大量連續(xù)內(nèi)存空間的Java對(duì)象,最典型的大對(duì)象就是很長(zhǎng)的字符串以及數(shù)組。大對(duì)象的虛擬機(jī)的內(nèi)存分配來(lái)說(shuō)是一個(gè)壞消息(尤其是遇到朝生夕死的短命大對(duì)象,寫(xiě)程序時(shí)應(yīng)避免),經(jīng)常出現(xiàn)大對(duì)象容易導(dǎo)致內(nèi)存還有不少空間時(shí)就提前觸發(fā)GC以獲取足夠的連續(xù)空間來(lái)安置他們。
虛擬機(jī)提供了一個(gè)參數(shù)-XX:PretenureSizeThreshold參數(shù),令大于這個(gè)設(shè)置的對(duì)象直接在老年代分配。這樣目的是避免在Eden區(qū)以及兩個(gè)Servivor區(qū)之間發(fā)生大量的內(nèi)存復(fù)制(新生代采用復(fù)制算法回收內(nèi)存)。
長(zhǎng)期存活的對(duì)象將進(jìn)入老年代
既然虛擬機(jī)采用了分代收集的思想來(lái)管理內(nèi)存,那么內(nèi)存回收就必須能識(shí)別那那些對(duì)象應(yīng)放在新生代,哪些對(duì)象應(yīng)放在老年代中,為了做到這一點(diǎn),虛擬機(jī)給每個(gè)對(duì)象定義了一個(gè)對(duì)象年齡(Age)計(jì)數(shù)器,如果在Eden出生并經(jīng)過(guò)第一次Mino Gc后仍然存活,并且能被Survivor容納的話,將被移動(dòng)到Survivor空間中年,并且對(duì)象年齡設(shè)為1.對(duì)象在Survivor區(qū)中每熬過(guò)一個(gè)Minor GC,年齡就增加1歲,當(dāng)他的年齡增加到一定程度(默認(rèn)為15歲),就將會(huì)晉升到老年代中。對(duì)象晉升老年代的年齡閾值,可以通過(guò)-XX:MaxTenuringThreshold設(shè)置。
動(dòng)態(tài)年齡判定
為了更好的適應(yīng)不同程序的內(nèi)存狀況,虛擬機(jī)并不是永遠(yuǎn)的要求對(duì)象的年齡必須達(dá)到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對(duì)象大小的總和大于Survivor空間的一半,年齡大于或等于的對(duì)象就可以直接進(jìn)入老年代,無(wú)須等到MaxTenuringThreshold中要求的年齡。
內(nèi)存分配擔(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),那么這是也會(huì)改為進(jìn)行一次Full GC。
前面提到過(guò),新生代使用的復(fù)制算法,但為了內(nèi)存利用率,只使用了一個(gè)Srurvivor空間來(lái)作為輪換備份,因此當(dāng)出現(xiàn)大量對(duì)象在Minor GC后仍然存活的情況(最極端的情況就是內(nèi)存回收后新生代中所有對(duì)象都存活),就需要老年代進(jìn)行分配擔(dān)保,把Survivor無(wú)法容納的對(duì)象直接進(jìn)入老年代與生活中的貸款擔(dān)保類似,老年代要進(jìn)行這樣的擔(dān)保,前提是老年代本身還有容納這些對(duì)象的剩余空間,一共有多少對(duì)象會(huì)活下來(lái)在實(shí)際完成內(nèi)存回收之前是無(wú)法明確知道的,所以只好取之前每一次回收晉升到老年代獨(dú)享容量的平均大小作為經(jīng)驗(yàn)值,與老年代的剩余空間進(jìn)行比較,決定是否進(jìn)行Full GC來(lái)讓老年代騰出更多時(shí)間。
取平均值進(jìn)行比較其實(shí)仍然是一種動(dòng)態(tài)概率的手段,也就是說(shuō),如果某次Minor GC存活后的對(duì)象突增,遠(yuǎn)遠(yuǎn)高于平均值,依然會(huì)導(dǎo)致擔(dān)保失敗(Handle Promotion Failure)。如果出現(xiàn)了Handle Promotion Failure失敗,那就只好在失敗后重新發(fā)起一次Full GC。雖然擔(dān)保失敗時(shí)繞的圈子是最大的,但是大部分情況下都還是將HandlePromotionFailure開(kāi)關(guān)打開(kāi),避免Full GC過(guò)于頻繁。
Full GC的觸發(fā)條件
對(duì)于Minor GC,其觸發(fā)條件非常簡(jiǎn)單,當(dāng)Eden空間滿時(shí),就會(huì)觸發(fā)一次Minor GC。而Full GC則相對(duì)復(fù)雜,因此我們主要介紹Full GC的觸發(fā)條件。
調(diào)用System.gc()
此方法的調(diào)用是建議JVM進(jìn)行一次Full GC,雖然只是建議而非一定,但很多情況下他會(huì)觸發(fā)Full GC,從而增加Full GC的頻率,也即增加了間接性停頓的次數(shù)。因此強(qiáng)烈建議能不使用此方法就不要使用,讓虛擬機(jī)自己去管理內(nèi)存,可通過(guò)-XX:+DisableExplicitGC來(lái)禁止RMI調(diào)用System.gc()。
老年代空間不足
老年代空間不足的常見(jiàn)場(chǎng)景為前文所講的大對(duì)象進(jìn)入老年代、長(zhǎng)期存活的對(duì)象進(jìn)入老年代等,當(dāng)執(zhí)行Full GC后空間仍然不足,則拋出如下錯(cuò)誤:Java.lang.OutOfMemoryError: Java heap space為避免以上兩種情況引起的Full GC,調(diào)優(yōu)時(shí)應(yīng)盡量讓對(duì)象在Minor GC階段被回收、讓對(duì)象在新生代多存活一段時(shí)間及不要?jiǎng)?chuàng)建過(guò)大的對(duì)象以及數(shù)組。
空間分配擔(dān)保失敗
前文提到,使用復(fù)制算法的Minor GC需要老年代的內(nèi)尺寸作擔(dān)保,如果出現(xiàn)了HandlePromotionFailure會(huì)觸發(fā)Full GC。
JDK1.7以及以前的永久代空間不足
在JDK1.7及以前,HotSpot虛擬機(jī)中的方法區(qū)是用永久代實(shí)現(xiàn)的,永久代中存放的為一些class的信息、常量、靜態(tài)變量等數(shù)據(jù),當(dāng)系統(tǒng)中要加載類、反射的類和調(diào)用方法較多時(shí),Permanet Generation可能會(huì)被占滿,在未配置為采用CMS GC的情況下也會(huì)執(zhí)行Full GC。如果經(jīng)過(guò)Full GC仍然回收不了,那么JVM會(huì)拋出如下錯(cuò)誤信息:Java.lang.OutOfMemoryError:PermGen space為避免PeremGen沾滿造成Full GC現(xiàn)象,可采用增大PermGen空間或轉(zhuǎn)為使用CMS GC。
在JDK1.8中用元空間替換了永久代作為方法區(qū)的實(shí)現(xiàn),元空間是本地內(nèi)存,因此減少了一種Full GC觸發(fā)的可能性。
Concurrent Mode Failure
執(zhí)行CMS GC過(guò)程中同時(shí)有對(duì)象要進(jìn)入老年代,而此時(shí)老年代空間不足(有時(shí)候空間不足是CMS GC當(dāng)前的浮動(dòng)垃圾過(guò)導(dǎo)致暫時(shí)性的空間不足觸發(fā)Full GC),便會(huì)報(bào)Concurrent Mode Failure錯(cuò)誤,并觸發(fā)Full GC。