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

如何判斷對(duì)象已死?

引用計(jì)數(shù)算法

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

引用計(jì)數(shù)算法的實(shí)現(xiàn)很簡(jiǎn)單,但有個(gè)巨大的缺點(diǎn),當(dāng)兩個(gè)對(duì)象相互引用時(shí),這兩個(gè)對(duì)象就不會(huì)被回收,導(dǎo)致內(nèi)存泄漏。

可達(dá)性分析算法

通過(guò)一系列的稱(chēng)為“GC Roots”的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開(kāi)始向下搜索,搜索所經(jīng)過(guò)的路徑稱(chēng)為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連(在圖論中稱(chēng)為對(duì)象不可達(dá))時(shí),這個(gè)對(duì)象就是不可用的。

在java語(yǔ)言中,可作為GC Roots的對(duì)象包括:

  • 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象
  • 方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象
  • 方法區(qū)中常量引用的對(duì)象
  • 本地方法棧中JNI引用的對(duì)象

關(guān)于java中的引用

jdk1.2之前,java中的引用指的就是另一塊內(nèi)存的起始地址。

jdk1.2之后,java對(duì)引用的概念進(jìn)行了擴(kuò)充,將引用分為:

  • 強(qiáng)引用(Strong Reference):在程序代碼中普遍存在,只要強(qiáng)引用還存在,垃圾回收器就不會(huì)回收被引用對(duì)象 ==> Object obj = new Object();
  • 軟引用(Soft Reference):是用來(lái)描述一些還有用但非必須的對(duì)象,在系統(tǒng)將要發(fā)生OutOfMemoryError時(shí),才會(huì)對(duì)被引用對(duì)象進(jìn)行回收 ==> SoftReference類(lèi)
  • 弱引用(Weak Reference):跟軟引用一樣也是來(lái)描述非必需對(duì)象的,但強(qiáng)度更弱,被引用對(duì)象只能存活到下次垃圾收集發(fā)生之前。無(wú)論當(dāng)前內(nèi)存是否充足,都會(huì)進(jìn)行回收。 ==> WeakReference類(lèi),WeakHashMap類(lèi)
  • 虛引用(Phantom Reference):也稱(chēng)為幽靈引用或幻影引用,是最弱的一種引用關(guān)系。一個(gè)對(duì)象是否有虛引用的存在,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無(wú)法通過(guò)虛引用來(lái)取得一個(gè)對(duì)象實(shí)例,唯一的目的就是能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知 WTF? ==> PhantomReference類(lèi)

生存還是死亡

在可達(dá)性分析算法中不可達(dá)的對(duì)象也不是“非死不可”,這個(gè)時(shí)候它們暫時(shí)處于“緩刑”階段,要真正宣告一個(gè)對(duì)象死亡,至少需要經(jīng)歷兩次標(biāo)記過(guò)程。當(dāng)一個(gè)對(duì)象被判定為不可達(dá)時(shí),會(huì)先進(jìn)行第一次標(biāo)記并判斷是否需要執(zhí)行finalize()方法。當(dāng)對(duì)象沒(méi)有覆蓋finalize()方法,或finalize()方法已經(jīng)被調(diào)用過(guò),則判定為不需要執(zhí)行finalize()方法。finalize()方法是對(duì)象逃脫死亡命運(yùn)的最后一次機(jī)會(huì),GC會(huì)將對(duì)象放置在F-Queue隊(duì)列中,由低優(yōu)先級(jí)的Finalizer線程去執(zhí)行。虛擬機(jī)會(huì)觸發(fā)finalize()方法,但不保證會(huì)等待它執(zhí)行結(jié)束,因?yàn)閒inalize()方法可能執(zhí)行緩慢或發(fā)生死循環(huán)。稍后GC會(huì)將F-Queue中的對(duì)象進(jìn)行第二次標(biāo)記,如果對(duì)象想要成功救贖自己就需要迅速的將自己與引用鏈上的任何一個(gè)對(duì)象建立關(guān)聯(lián),那么在第二次標(biāo)記時(shí)它將會(huì)被移出即將回收的集合。

回收方法區(qū)

很多人認(rèn)為方法區(qū)(HotSopt中的永久代)是沒(méi)有垃圾收集的,java虛擬機(jī)規(guī)范中也沒(méi)有要求需要對(duì)方法區(qū)實(shí)現(xiàn)垃圾收集。

永久代垃圾收集主要回收兩部分:

  • 廢棄常量

回收廢棄常量與回收java堆中的對(duì)象非常相似。例如常量池中的“abc”字符串在當(dāng)前系統(tǒng)中沒(méi)有被任何一個(gè)String對(duì)象引用,也沒(méi)有在其他地方被引用,而且必要的話,這個(gè)“abc”常量就會(huì)被系統(tǒng)清理出常量池。

  • 無(wú)用的類(lèi)

類(lèi)需要滿足下面3個(gè)條件才能算是“無(wú)用類(lèi)”。

  1. 該類(lèi)的所有實(shí)例都已經(jīng)被回收
  2. 加載該類(lèi)的ClassLoader已經(jīng)被回收
  3. 該類(lèi)對(duì)應(yīng)的Class對(duì)象沒(méi)有在任何地方被引用,即無(wú)法通過(guò)反射獲取該類(lèi)的方法

虛擬機(jī)可以對(duì)無(wú)用類(lèi)進(jìn)行回收,但不是一定會(huì)回收

垃圾收集算法

標(biāo)記-清除算法

標(biāo)記-清除算法是最基礎(chǔ)的收集算法,分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收。之所以所它是最基礎(chǔ)的收集算法,是因?yàn)楹罄m(xù)的收集算法都是基于這個(gè)思路并對(duì)其不足進(jìn)行改進(jìn)而得到的。

主要不足有兩個(gè):

  • 效率,標(biāo)記和清除兩個(gè)過(guò)程的效率都不高。
  • 空間,標(biāo)記清除后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,導(dǎo)致以后需要分配較大對(duì)象時(shí),無(wú)法找到足夠的連續(xù)內(nèi)存而不得提前觸發(fā)垃圾收集動(dòng)作。
“標(biāo)記-清除”算法示意圖

復(fù)制算法

為了解決效率問(wèn)題,一種稱(chēng)為“復(fù)制”的收集算法出現(xiàn)了,它將可用內(nèi)存分為大小相等的兩塊,每次只使用其中一塊。當(dāng)這塊內(nèi)存用完了,就將還存活的對(duì)象復(fù)制到另一塊上,然后將已使用過(guò)的內(nèi)存空間一次清理掉。這樣就不需要考慮內(nèi)存碎片等復(fù)雜情況,簡(jiǎn)單高效,是典型的空間換時(shí)間,內(nèi)存消耗太大。

復(fù)制算法示意圖

現(xiàn)在的商業(yè)虛擬機(jī)都采用這種收集算法來(lái)回收新生代,由于新生代中98%的對(duì)象是“朝生夕死”的,所以不需要按照1:1的比例來(lái)劃分空間,而是把內(nèi)存分為一塊較大的Eden區(qū)和兩塊較小的Survivor區(qū),每次使用Eden和其中一塊Survivor。HotSpot虛擬機(jī)默認(rèn)Eden:Survivor = 8:1。當(dāng)然我們無(wú)法保證每次回收都只有不多于10%的對(duì)象存活,當(dāng)Survivor空間不足是,需要依賴(lài)其他內(nèi)存(這里是老年代)進(jìn)行分配擔(dān)保(Handle Promotion),多余的對(duì)象會(huì)直接通過(guò)分配擔(dān)保進(jìn)制進(jìn)入老年代。

標(biāo)記-整理算法

復(fù)制收集算法在對(duì)象存活率較高時(shí)就要進(jìn)行較多的復(fù)制操作,效率將會(huì)降低,而且還需要額外空間進(jìn)行分配擔(dān)保,以應(yīng)對(duì)被使用內(nèi)存中對(duì)象100%存活的極端情況,所以在老年代一般不能直接選擇這種算法。標(biāo)記-整理算法就是根據(jù)老年代的特點(diǎn)設(shè)計(jì)的。該算法第一步與標(biāo)記-清除算法一樣,第二步則是將所有存活對(duì)象都向一端移動(dòng),然后直接清理掉其他內(nèi)存。

“標(biāo)記-整理”算法示意圖

分代收集算法

目前商業(yè)虛擬機(jī)垃圾收集都采用“分代收集”(Generational Collection)算法,其原理就是根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊,如新生代和老年代,然后根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴?/strong>。

GC算法實(shí)現(xiàn)基礎(chǔ)

枚舉根節(jié)點(diǎn)

在可達(dá)性分析中,GC必須先從GC Roots節(jié)點(diǎn)找引用鏈,然后逐個(gè)檢查里面的引用,可作為GC Roots的節(jié)點(diǎn)主要在全局性引用(例如常量或類(lèi)靜態(tài)屬性)與執(zhí)行上下文(例如棧幀中的本地變量表)中。另外,可達(dá)性分析必須在一個(gè)能確保一致性的快照中進(jìn)行,這里的一致性是指在整個(gè)分析期間整個(gè)系統(tǒng)看起來(lái)就像被凍結(jié)在某個(gè)時(shí)間點(diǎn)上,不可以出現(xiàn)對(duì)象引用關(guān)系還在不斷變化的情況,該點(diǎn)不滿足的話分析結(jié)果準(zhǔn)確性就無(wú)法得到保證。這點(diǎn)是導(dǎo)致GC進(jìn)行時(shí)必須停頓所有java執(zhí)行線程(Stop The World)的其中一個(gè)重要原因,即使在號(hào)稱(chēng)幾乎不會(huì)發(fā)生停頓的CMS收集器中,枚舉根節(jié)點(diǎn)時(shí)也是必須要停頓的。

安全點(diǎn)

由于程序在運(yùn)行時(shí)導(dǎo)致引用變化的指令非常多,因此在程序執(zhí)行時(shí)并非在所有地方都能停頓下來(lái)開(kāi)始GC,只有到達(dá)特定的位置即安全點(diǎn)(Safepoint)才能暫停。

Safepoint的選定既不能太少以致于讓GC等待時(shí)間太長(zhǎng),也不能太多以致于過(guò)分增大運(yùn)行時(shí)的負(fù)荷,還有一個(gè)需要考慮的問(wèn)題就是如何在GC發(fā)生時(shí)讓所有線程都“跑”到最近的安全點(diǎn)上再停頓下來(lái)。這里有兩種方案可供選擇:

  • 搶先式中斷(Preemptive Suspension):不需要線程執(zhí)行代碼主動(dòng)區(qū)配合,在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)式中斷(Voluntary Suspension):當(dāng)GC需要中斷線程時(shí),不直接對(duì)線程進(jìn)行操作,僅僅簡(jiǎn)單地設(shè)置一個(gè)標(biāo)志,各個(gè)線程執(zhí)行時(shí)主動(dòng)去輪詢這個(gè)標(biāo)志,發(fā)現(xiàn)中斷標(biāo)志為真時(shí)就自己中斷掛起。

安全區(qū)域

對(duì)于Safepoint來(lái)說(shuō),有一個(gè)最大的缺點(diǎn)就是需要等待線程走到安全的地方去中斷掛起,例如Sleep狀態(tài)或Blocked狀態(tài)。這個(gè)時(shí)候線程無(wú)法響應(yīng)JVM的中斷請(qǐng)求,顯然JVM也不大可能等待線程重新被分配CPU事件。對(duì)于這個(gè)情況,就需要安全區(qū)域(Safe Region)來(lái)解決。安全區(qū)域是指在一段代碼片段中,引用關(guān)系不會(huì)發(fā)生變化,在這個(gè)區(qū)域的任何地方開(kāi)始GC都是安全的。當(dāng)線程執(zhí)行到Safe Region中的代碼時(shí),首先標(biāo)識(shí)自己已經(jīng)進(jìn)入了Safe Region,這樣,在這段時(shí)間內(nèi)JVM要發(fā)起GC時(shí),就不用管標(biāo)識(shí)自己為Safe Region狀態(tài)的線程了。當(dāng)線程要離開(kāi)Safe Region時(shí),要檢查系統(tǒng)是否完成了根節(jié)點(diǎn)枚舉(或者整個(gè)GC過(guò)程),如果完成則繼續(xù)執(zhí)行,否則就必須等待直到可以安全離開(kāi)Safe Region的信號(hào)為止。Safe Region可以看作是Safepoint的擴(kuò)展。

垃圾收集器

這里以JDK 1.7 Update 14之后的HotSpot虛擬機(jī)為例

HotSpot虛擬機(jī)的垃圾收集器

圖中展示了7中作用與不同分代的收集器,如果兩個(gè)收集器之間存在連線,就說(shuō)明它們可以搭配使用。虛擬機(jī)所在區(qū)域表示它屬于新生代收集器還是老年代收集器。

Serial收集器

Serial收集器是最基本、發(fā)展歷史最悠久的收集器,該收集器是一個(gè)單線程的收集器。這個(gè)單線程說(shuō)明它只會(huì)使用一個(gè)CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進(jìn)行垃圾收集時(shí),必須暫停其他所有的工作線程(Stop The World),直到收集結(jié)束。

Serial收集器運(yùn)行示意圖

ParNew收集器

ParNew收集器其實(shí)就是Serial收集器的多線程版本。

ParNew收集器運(yùn)行示意圖

從ParNew收集器開(kāi)始,后面還會(huì)接觸到幾款并發(fā)和并行的收集器。在這之前有必要了解并發(fā)和并行:

  • 并行(Parallel):指多條垃圾收集線程并行工作,但此時(shí)用戶相似仍然處于等待狀態(tài)
  • 并發(fā)(Concurrent):指用戶線程與垃圾收集線程同時(shí)執(zhí)行(但不一定是并行的,可能會(huì)交替執(zhí)行),用戶程序在繼續(xù)執(zhí)行,而垃圾收集程序運(yùn)行在另一個(gè)CPU上。

Parallel Scavenge收集器

Parallel Scavenge收集器是一個(gè)新生代收集器,使用復(fù)制算法與并行多線程。它的特點(diǎn)在于關(guān)注點(diǎn)與其他收集器不同,目的是為了達(dá)到一個(gè)可控制的吞吐量(Throughput)。所謂的吞吐量 = 運(yùn)行用戶代碼時(shí)間/(運(yùn)行用戶代碼事件+垃圾收集時(shí)間),垃圾收集時(shí)間越短響應(yīng)速度越快,吞吐量越大運(yùn)算速度越快。對(duì)于Parallel Scavenge收集器來(lái)說(shuō)還有一個(gè)重要參數(shù)-XX:+UseAdaptiveSizePolicy,當(dāng)這個(gè)參數(shù)打開(kāi)后,就不需要手動(dòng)指定新生代的大小、Eden與Survivor區(qū)的比例、晉升老年代對(duì)象年齡等細(xì)節(jié)參數(shù)了,虛擬機(jī)會(huì)自動(dòng)根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控信息,動(dòng)態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時(shí)間或者最大的吞吐量,這種調(diào)節(jié)方式稱(chēng)為GC自適應(yīng)調(diào)節(jié)策略(GC Ergonomics),這是Parallel Scavenge收集器與ParNew收集器的一個(gè)重要區(qū)別。

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,使用單線程和標(biāo)記-整理算法。

Serial Old收集器運(yùn)行示意圖

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和標(biāo)記-整理算法。

Parallel Old收集器運(yùn)行示意圖

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器,使用多線程和標(biāo)記-清除算法,對(duì)于重視響應(yīng)速度的應(yīng)用來(lái)說(shuō)十分適合。整個(gè)運(yùn)作過(guò)程分為4步:

  • 初始標(biāo)記(CMS initial mark):僅僅只標(biāo)記GC Roots能直接關(guān)聯(lián)到的對(duì)象
  • 并發(fā)標(biāo)記(CMS concurrent mark):進(jìn)行GC追蹤(GC Roots Tracing)的過(guò)程
  • 重新標(biāo)記(CMS remark):修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄,這個(gè)階段停頓時(shí)間一般會(huì)比初始標(biāo)記階段稍長(zhǎng)一點(diǎn),但遠(yuǎn)比并發(fā)標(biāo)記的時(shí)間短
  • 并發(fā)清除(CMS concurrent sweep):對(duì)標(biāo)記對(duì)象進(jìn)行清除
Concurrent Mark Sweep收集器運(yùn)行示意圖

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

  1. 對(duì)CPU資源非常敏感:在并發(fā)階段,會(huì)占用一部分CPU資源,從而導(dǎo)致應(yīng)用程序變慢,總吞吐量降低
  2. 無(wú)法處理浮動(dòng)垃圾(Floating Garbage):浮動(dòng)垃圾是指在并發(fā)清理階段用核線程還在運(yùn)行并產(chǎn)生新的垃圾,這部分垃圾出現(xiàn)在標(biāo)記之后,CMS無(wú)法在當(dāng)此處理掉它們,只好等待下一次GC。這意味著CMS收集器必須預(yù)留一部分空間提供并發(fā)收集的程序使用,會(huì)導(dǎo)致GC頻率增加。
  3. 基于標(biāo)記-清除算法

G1收集器

G1收集器的優(yōu)點(diǎn)

  • 并行與并發(fā)
  • 分代收集
  • 空間整合
  • 可預(yù)測(cè)的停頓

在G1之前的其他收集器進(jìn)行收集時(shí),都是對(duì)整個(gè)新生代或者老年代進(jìn)行回收,而G1收集器不再是這樣。G1收集器將java堆分為多個(gè)獨(dú)立區(qū)域(Region),每個(gè)區(qū)域之間相互獨(dú)立,多個(gè)Region共同組成Eden、Survivor和老年代。這樣G1收集器在進(jìn)行垃圾收集時(shí),就可以以Region為單位進(jìn)行,而不需要對(duì)整個(gè)java堆進(jìn)行全區(qū)域的垃圾收集。此時(shí)還有兩個(gè)問(wèn)題:

  1. 對(duì)象在所在Region死亡時(shí),并不意味著對(duì)于java堆中其他的Region也是死亡的。難道還需要掃描這個(gè)java堆才能保證準(zhǔn)確性?
  • 對(duì)于這個(gè)問(wèn)題,虛擬機(jī)時(shí)采用Remembered Set來(lái)避免全堆掃描的。每個(gè)Region都有相對(duì)應(yīng)的Remembered Set,用于記錄其他Region中的引用信息,這樣就可以避免全堆掃描。
  1. 對(duì)象大小超過(guò)Region最大容量
  • 虛擬機(jī)中定義了巨無(wú)霸區(qū)域(Humongous regions)來(lái)存儲(chǔ)大對(duì)象,但是對(duì)于大對(duì)象的回收貌似沒(méi)有什么好辦法。

ps:在java8中,持久代也移動(dòng)到了普通的堆內(nèi)存中,改為元空間。

G1收集器運(yùn)行示意圖

對(duì)于具體的運(yùn)作流程,可以看看這兩篇文章G1垃圾收集器入門(mén)、深入理解 Java G1 垃圾收集器

其他的一些策略

  • 對(duì)象優(yōu)先在Eden分配
  • 大對(duì)象直接進(jìn)入老年代
  • 長(zhǎng)期存活對(duì)象將進(jìn)入老年代
  • 動(dòng)態(tài)對(duì)象年齡判定
  • 空間分配擔(dān)保

參考文章

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

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