JVM 垃圾收集器(Garbage Collection)
判斷對(duì)象是否存活
在堆里邊存放著java世界中幾乎所有的對(duì)象實(shí)例,垃圾收集器在對(duì)堆進(jìn)行回收前,首先需要確定這些對(duì)象之中哪些還“存活”著,哪些已經(jīng)“死去”(即不可能再被任何途徑使用的對(duì)象)。
引用計(jì)數(shù)算法
給對(duì)象添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器值就加1;當(dāng)引用失效時(shí),計(jì)數(shù)器值就減1;任何時(shí)刻計(jì)數(shù)器為0的對(duì)象就是不可能再被使用的。該方法簡(jiǎn)單,但也有一個(gè)缺點(diǎn)是很難解決對(duì)象之間相互循環(huán)引用的問(wèn)題。
可達(dá)性分析算法
該算法的基本思路就是通過(guò)一系列的稱為“GCRoots”的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開(kāi)始向下搜索,搜索所走過(guò)的路徑稱為引用鏈。當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連時(shí),則證明此對(duì)象是不可用的。在java語(yǔ)言中,可作為GC Roots的對(duì)象包括下面幾種:
再談引用
引用分為強(qiáng)引用、軟引用、弱引用和虛引用。
- 強(qiáng)引用:代碼中明確指明的,如“Object a = new Object()”,只要強(qiáng)引用還在,就不會(huì)被GC
- 軟引用:被軟應(yīng)用關(guān)聯(lián)對(duì)象第一次發(fā)生GC時(shí)可以躲過(guò),第二次遇到GC時(shí)才會(huì)被回收
- 弱引用:被弱引用關(guān)聯(lián)的對(duì)象在發(fā)生GC時(shí)就會(huì)被回收
- 虛引用:也稱為幽靈引用,它是最弱的一種引用關(guān)系。一個(gè)對(duì)象是否有虛引用的存在,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無(wú)法通過(guò)虛引用來(lái)取得一個(gè)對(duì)象實(shí)例。為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個(gè)對(duì)象唄垃圾收集器回收時(shí)受到一個(gè)系統(tǒng)通知。
生存還是死亡
即使在可達(dá)性分析算法中不可達(dá)的對(duì)象,也并非是“非死不可”的。要真正宣告一個(gè)對(duì)象死亡,至少要經(jīng)歷兩次標(biāo)記過(guò)程:如果對(duì)象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒(méi)有與GC Roots相連接的引用鏈,那它將會(huì)被第一次標(biāo)記并且進(jìn)行一次篩選,篩選的條件是此對(duì)象是否有必要執(zhí)行finalize()方法。當(dāng)對(duì)象沒(méi)有覆蓋finalize()方法,或者finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過(guò),虛擬機(jī)將這兩種情況都視為“沒(méi)有必要執(zhí)行”。
如果這個(gè)對(duì)象唄判定為有必要執(zhí)行finalize()方法,那么這個(gè)對(duì)象將會(huì)放置在一個(gè)叫做F-Queue的隊(duì)列中,并在稍后由一個(gè)由虛擬機(jī)自動(dòng)建立的、低優(yōu)先級(jí)的Finalizer線程去執(zhí)行它。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í)它將被移除出“即將回收”的集合。
回收方法區(qū)
方法區(qū)(或者HotSpot虛擬機(jī)中的永久代)的垃圾收集主要回收兩部分內(nèi)容:廢棄常量和無(wú)用的類。回收廢棄常量與回收java堆中的對(duì)象非常相似。以常量池中字面量的回收為例,假設(shè)一個(gè)字符串”abc”已經(jīng)進(jìn)入了常量池中,但是當(dāng)前系統(tǒng)沒(méi)有任何一個(gè)String對(duì)象值為”abc”,換句話說(shuō),就是沒(méi)有任何String對(duì)象引用常量池中的”abc”常量,也沒(méi)有其他地方引用了這個(gè)字面量,如果這時(shí)發(fā)生內(nèi)存回收,而且必要的話,這個(gè)”abc”常量就會(huì)被系統(tǒng)清理出常量池。常量池中的其他類(接口)、方法、字段的符號(hào)引用也與此類似。
判定一個(gè)常量是否是“廢棄常量”比較簡(jiǎn)單,而要判定一個(gè)類是否是“無(wú)用的類”的條件則相對(duì)苛刻的多,需要同時(shí)滿足一下3個(gè)條件:
在大量使用發(fā)射、動(dòng)態(tài)代理、CGLib等ByteCode框架、動(dòng)態(tài)生成JSP以及OSGi這類頻繁自定義ClassLoader的場(chǎng)景都需要虛擬機(jī)具備類卸載的功能,以保證永久代不會(huì)溢出。
垃圾收集算法
1. 標(biāo)記-清除算法(Mark-Sweep)?
首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對(duì)象。這種算法的主要不足有兩個(gè):一個(gè)是效率問(wèn)題,標(biāo)記和清楚兩個(gè)過(guò)程的效率都不高;另一個(gè)是空間問(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)作。
2. 復(fù)制算法(Copying)
復(fù)制算法適用于新生代,因?yàn)樵谛律?#xff0c;垃圾對(duì)象通常會(huì)多余存活對(duì)象,復(fù)制算法效果會(huì)比較好。為了解決效率問(wèn)題,可以將內(nèi)存按照容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這塊的內(nèi)存用完了,就將還存活的對(duì)象復(fù)制到另一塊上面,然后再把已使用過(guò)的內(nèi)存空間一次清理掉。這種方法實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效,代價(jià)是將內(nèi)存縮小為了原來(lái)的一半。
為了提高內(nèi)存的利用率,可以將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當(dāng)回收時(shí),將Eden和Survivor中還存活著的對(duì)象一次性地復(fù)制到另外一塊Survivor空間上,最后清理掉Eden和剛剛用過(guò)的Survivor空間。
為什么需要有兩塊Survivor? 這是因?yàn)閟urvivor中的對(duì)象在達(dá)到“老年”(由指定參數(shù)-XX:MaxTenuringThreshold決定)之前肯定有對(duì)象已經(jīng)變成“垃圾”了,這時(shí)候必須要對(duì)其進(jìn)行回收,如果只使用一個(gè)survivor的話,那么要不容忍survivor存在內(nèi)存碎片,要么要對(duì)其進(jìn)行內(nèi)存整理,出于和對(duì)Eden區(qū)域同樣的考慮,所以實(shí)際上對(duì)Survivor的GC也是基于復(fù)制算法的,不過(guò)是從一個(gè)Survivor到另外一個(gè)Survivor(這也是GC日志中為什么叫from space和to space),所以Survivor的兩個(gè)區(qū)是對(duì)稱的,沒(méi)有先后關(guān)系,所以Survivor區(qū)中可能同時(shí)存在從Eden復(fù)制過(guò)來(lái)對(duì)象,以及從前一個(gè)Survivor復(fù)制過(guò)來(lái)的對(duì)象,某一次GC結(jié)束時(shí)肯定會(huì)有一個(gè)Survivor是空的。
3. 標(biāo)記-整理算法(Mark-Compact)
復(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)記過(guò)程仍然與“標(biāo)記-清除”算法中的一樣,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。
4. 分代收集算法
一般把JVM的堆分為新生代和老年代,這樣可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴āT谛律?#xff0c;選用復(fù)制算法;而老年代中因?yàn)閷?duì)象存活率高,沒(méi)有額外空間對(duì)它進(jìn)行分配擔(dān)保,就采用“標(biāo)記-清理”或者“標(biāo)記-整理”算法來(lái)進(jìn)行回收。
5. 分區(qū)收集算法
分區(qū)收集算法是將整個(gè)堆空間劃分為連續(xù)的不同小區(qū)間,每個(gè)小區(qū)間獨(dú)立使用,獨(dú)立回收,G1收集器就是使用該算法。
HotSpot中垃圾收集算法的實(shí)現(xià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í)間。
另外,可達(dá)性分析對(duì)執(zhí)行時(shí)間的敏感還體現(xiàn)在GC停頓上,因?yàn)檫@項(xiàng)分析工作必須在一個(gè)能確保一致性的快照中進(jìn)行——這里“一致性”的意思是指在整個(gè)分析起見(jiàn)整個(gè)執(zhí)行系統(tǒng)看起來(lái)就像被凍結(jié)在某個(gè)時(shí)間點(diǎn)上,不可以出現(xiàn)分析過(guò)程中對(duì)象引用關(guān)系還在不斷變化的情況,該點(diǎn)不滿足的話分析結(jié)果準(zhǔn)確性就無(wú)法得到保證。
對(duì)于主流的Java虛擬機(jī),當(dāng)執(zhí)行系統(tǒng)停頓下來(lái)后,并不需要一個(gè)不漏地檢查完所有執(zhí)行上下文和全局的引用位置,虛擬機(jī)應(yīng)該是有辦法直接得知哪些地方存放著對(duì)象引用。對(duì)于HotSpot,是使用了一組稱為OopMap的數(shù)據(jù)結(jié)構(gòu)來(lái)達(dá)到這個(gè)目的的,在類加載完成的時(shí)候,HotSpot就把對(duì)象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計(jì)算出來(lái),在JIT編譯過(guò)程中,也會(huì)在特定的位置記錄下棧和寄存器中哪些位置是引用。這樣,GC在掃描時(shí)就可以直接得知這些信息了。
安全點(diǎn)
在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,那將會(huì)需要大量的額外空間,這樣GC的空間成本將會(huì)變得很高。
實(shí)際上,HotSpot只是在“特定的位置”記錄了這些信息,這些位置成為安全點(diǎn)。即程序執(zhí)行時(shí)并非在所有地方都能停頓下來(lái)開(kāi)始GC,只有在到達(dá)安全點(diǎn)時(shí)才能暫停。安全點(diǎn)的選定既不能太少以至于讓GC等待時(shí)間太長(zhǎng),也不能過(guò)于頻繁以至于過(guò)分增大運(yùn)行時(shí)的負(fù)荷。所以安全點(diǎn)的選擇基本上是以程序“是否具有讓程序長(zhǎng)時(shí)間執(zhí)行的特征”為標(biāo)準(zhǔn)。“長(zhǎng)時(shí)間執(zhí)行”的最明顯特征就是指令序列復(fù)用,例如方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等。
對(duì)于安全點(diǎn),另一個(gè)需要考慮從的問(wèn)題是如何在GC發(fā)生時(shí)讓所有線程都“跑”到最近的安全點(diǎn)上再停頓下來(lái)。這里有兩種方案:搶先式中斷和主動(dòng)式中斷(使用較多)。搶先式中斷是在GC發(fā)生時(shí),首先把所有線程全部中斷,如果發(fā)現(xiàn)有線程中斷的地方不在安全點(diǎn)上,就恢復(fù)線程,讓它跑到安全點(diǎn)上;而主動(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í)就自己中斷掛起。
安全區(qū)域
安全點(diǎn)機(jī)制保證了程序執(zhí)行時(shí),在不太長(zhǎng)的時(shí)間內(nèi)就會(huì)遇到可進(jìn)入GC的安全點(diǎn)。但是,程序“不執(zhí)行”的時(shí)候呢?比如沒(méi)有分配CPU時(shí)間,典型的例子就是線程處于Sleep或者Blocked狀態(tài),這時(shí)候線程無(wú)法響應(yīng)JVM的中斷請(qǐng)求,從而“走”到安全的地方去中斷掛起,JVM也顯然不太可能等待線程重新被分配CPU時(shí)間,對(duì)于這種情況,就需要安全區(qū)域來(lái)解決。
安全區(qū)域是指一段代碼片段之中,引用關(guān)系不會(huì)發(fā)生變化。在這個(gè)區(qū)域中的任何地方開(kāi)始GC都是安全的。在線程執(zhí)行到安全區(qū)域后,首先標(biāo)識(shí)自己已經(jīng)進(jìn)入安全區(qū)域,當(dāng)在這段時(shí)間里JVM要發(fā)起GC時(shí),就不用管標(biāo)識(shí)自己為安全區(qū)域狀態(tài)的線程了。在線程要離開(kāi)安全區(qū)域時(shí),它要檢查系統(tǒng)是否已經(jīng)完成了根節(jié)點(diǎn)枚舉(或者是整個(gè)GC過(guò)程),如果完成了,那線程就繼續(xù)執(zhí)行,否則它就必須等待知道收到可以安全離開(kāi)安全區(qū)域的信號(hào)為止。
垃圾收集器
串行收集器
新生代串行收集器Serial GC是基于復(fù)制算法,實(shí)現(xiàn)簡(jiǎn)單,處理高效。老年代串行收集器Serial Old GC使用的是標(biāo)記整理算法。Serial收集器是最簡(jiǎn)單的一個(gè),是一個(gè)單線程的收集器,它在工作的時(shí)候會(huì)將所有應(yīng)用線程全部?jī)鼋Y(jié)。
如何使用它:你可以打開(kāi)-XX:+UseSerialGC這個(gè)JVM參數(shù)來(lái)使用它。
?
?
與串行收集器相關(guān)的參數(shù):
-XX:+UseSerialGC:在新生代和老年代使用串行收集器
-XX:SurvivorRatio:設(shè)置Eden區(qū)大小和Survivor區(qū)大小的比例;默認(rèn)是8,即Eden:Survivor=8:1
-XX:PretenureSizeThreshold:設(shè)置大對(duì)象直接進(jìn)入老年代的對(duì)象大小閾值。
-XX:MaxTenuringThreshold:設(shè)置對(duì)象進(jìn)入老年代的年齡的最大值;默認(rèn)是15歲,出生就已經(jīng)是1歲了。
并行收集器
ParNew收集器
新生代ParNew收集器ParNewGC是串行收集器的多線程版本
則老年代默認(rèn)使用SerialOldGC收集器,老年代也可以修改成CMS
Parallel Scavenge收集器
新生代ParallelGC收集器也是使用復(fù)制算法,但它非常關(guān)注系統(tǒng)的吞吐量。
老年代ParallelOldGC收集器也是一種關(guān)注吞吐量的收集器,使用標(biāo)記-整理算法。
所謂吞吐量就是CPU用于運(yùn)行用戶代碼的時(shí)間與CPU總消耗時(shí)間的比值。吞吐量=運(yùn)行用戶代碼時(shí)間/(運(yùn)行用戶代碼時(shí)間+垃圾收集時(shí)間)。
與并行GC相關(guān)的參數(shù):
-XX:+UseParNewGC:在新生代使用并行收集器;
-XX:+UseParallelGC:設(shè)置并行新生代收集器(吞吐量?jī)?yōu)先)
-XX:+UseParallelOldGC:老年代使用并行收集器;
-XX:ParallelGCThreads:設(shè)置用于垃圾回收的線程數(shù)。通常和CPU數(shù)量相等;
-XX:MaxGCPauseMillis:設(shè)置最大垃圾收集停頓時(shí)間;
-XX:GCTimeRatio:設(shè)置吞吐量大小,0到100之間;默認(rèn)是99,如99就代表:運(yùn)行用戶代碼時(shí)間/(運(yùn)行用戶代碼時(shí)間+垃圾收集時(shí)間)=99%
CMS收集器
CMS收集器(concurrent-mark-sweep)是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。其使用了多個(gè)線程(concurrent)來(lái)掃描堆并標(biāo)記(mark)那些不再使用的可以回收(sweep)的對(duì)象。主要步驟有:初始標(biāo)記、并發(fā)標(biāo)記、預(yù)清理、重新標(biāo)記、并發(fā)清除和并發(fā)重置。這個(gè)算法在兩種情況下會(huì)進(jìn)入一個(gè)”stop the world”的模式:當(dāng)進(jìn)行根對(duì)象的初始標(biāo)記的時(shí)候 (老生代中線程入口點(diǎn)或靜態(tài)變量可達(dá)的那些對(duì)象)以及當(dāng)這個(gè)算法在并發(fā)運(yùn)行的時(shí)候應(yīng)用程序改變了堆的狀態(tài)使得它不得不回去再次確認(rèn)自己標(biāo)記的對(duì)象都是正確的。
雖然稱之為并發(fā)低停頓收集器,但是它有以下3個(gè)缺點(diǎn):
G1回收器
G1( Garbage first)回收器在JDK 7update 4中首次引入,與其他收集器相比,G1具有以下特點(diǎn):
并行與并發(fā):G1能充分利用多CPU、多核環(huán)境下的硬件優(yōu)勢(shì),使用多個(gè)CPU或者CPU核心來(lái)縮短Stop-The-World停頓的時(shí)間;
分代收集:與其他收集器一樣,分代概念在G1中依然得以保留;
空間整合:與CMS的“標(biāo)記-清理”算法不同,G1從整體來(lái)看是基于“標(biāo)記-整理”算法實(shí)現(xiàn)的收集器,從局部來(lái)看是基于“復(fù)制”算法實(shí)現(xiàn)的,兩種算法都不會(huì)產(chǎn)生內(nèi)存空間碎片;
可預(yù)測(cè)的停頓:降低停頓時(shí)間是G1和CMS共同的關(guān)注點(diǎn),但G1除了追求低停頓外,還能建立可預(yù)測(cè)的停頓時(shí)間模型,能讓使用者明確指定在一個(gè)長(zhǎng)度為M毫秒的時(shí)間片段內(nèi),消耗在垃圾收集的時(shí)間不得超過(guò)N毫秒。
G1算法將堆劃分為若干個(gè)區(qū)域(Region),它仍然屬于分代收集器。不過(guò),這些區(qū)域的一部分包含新生代,新生代的垃圾收集依然采用暫停所有應(yīng)用線程的方式,將存活對(duì)象拷貝到老年代或者Survivor空間。老年代也分成很多區(qū)域,G1收集器通過(guò)將對(duì)象從一個(gè)區(qū)域復(fù)制到另外一個(gè)區(qū)域,完成了清理工作。這就意味著,在正常的處理過(guò)程中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會(huì)有cms內(nèi)存碎片問(wèn)題的存在了。
在G1中,還有一種特殊的區(qū)域,叫Humongous區(qū)域。 如果一個(gè)對(duì)象占用的空間超過(guò)了分區(qū)容量50%以上,G1收集器就認(rèn)為這是一個(gè)巨型對(duì)象。這些巨型對(duì)象,默認(rèn)直接會(huì)被分配在年老代,但是如果它是一個(gè)短期存在的巨型對(duì)象,就會(huì)對(duì)垃圾收集器造成負(fù)面影響。為了解決這個(gè)問(wèn)題,G1劃分了一個(gè)Humongous區(qū),它用來(lái)專門存放巨型對(duì)象。如果一個(gè)H區(qū)裝不下一個(gè)巨型對(duì)象,那么G1會(huì)尋找連續(xù)的H分區(qū)來(lái)存儲(chǔ)。為了能找到連續(xù)的H區(qū),有時(shí)候不得不啟動(dòng)Full GC。
詳細(xì)請(qǐng)看:https://www.cnblogs.com/ASPNET2008/p/6496481.html
https://blog.csdn.net/j3T9Z7H/article/details/80074460
相關(guān)參數(shù):
-XX:+UseG1GC:使用G1收集器
-XX:MaxGCPauseMillis:設(shè)置最大垃圾收集停頓時(shí)間;
-XX:G1HeapRegionSize:使用G1時(shí)Java堆會(huì)被分為大小統(tǒng)一的的區(qū)(region)。此參數(shù)可以指定每個(gè)heap區(qū)的大小. 默認(rèn)值將根據(jù) heap size 算出最優(yōu)解. 最小值為 1Mb, 最大值為 32Mb;
?
如果你追求低停頓,那G1是個(gè)不錯(cuò)的選擇。雖然G1沒(méi)有太犧牲吞吐量,但如果你追求吞吐量,那么G1并不會(huì)為你帶來(lái)什么特別的好處。
?
擴(kuò)展閱讀:HotSpot JVM默認(rèn)垃圾收集器
總結(jié)
以上是生活随笔為你收集整理的JVM 垃圾收集器(Garbage Collection)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 8086指令系统中的寻址方式
- 下一篇: [转]关于卢平的一些想法