垃圾收集器和內(nèi)存分配策略

可達(dá)性分析算法

可達(dá)性算法圖解

當(dāng)一個(gè)對象到GC Roots沒有任何引用鏈(用圖論的話說,就是從GC Roots節(jié)點(diǎn)觸發(fā)到某個(gè)節(jié)點(diǎn)不可達(dá))時(shí),則證明此對象是不可用的。也就是說該對象是可回收的。

什么是GC Roots?

在Java語言中,可作為GC Roots的對象包括下面幾種。

  • 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象
  • 方法區(qū)中類靜態(tài)屬性引用的對象
  • 方法區(qū)中常量引用的對象
  • 本地方法棧中JNI(即一般所說的Native方法)引用的對象

對象何時(shí)宣告死亡

要真正宣告一個(gè)對象死亡,至少需要經(jīng)歷兩次標(biāo)記過程:

  1. 如果對象在經(jīng)歷一次可達(dá)性分析之后發(fā)現(xiàn)沒有與 GC Roots 相連接的引用鏈,那么他將會被第一次標(biāo)記。

  2. 如果這個(gè)對象被判定有必要執(zhí)行 finalize() 方法,那么這個(gè)方法將會被放置在一個(gè)叫做 F-Queue 的隊(duì)列中,并在稍后由一個(gè)虛擬機(jī)建立的,低優(yōu)先級的 Finalizer 線程去執(zhí)行它。稍后GC將對 F-Queue 中的對象進(jìn)行第二次小規(guī)模標(biāo)記。

兩次標(biāo)記成功之后,GC便會將這個(gè)對象收回,對象宣告死亡。

那么第一次被標(biāo)記之后有沒有辦法逃脫GC回收呢?

答案是有的,在進(jìn)入 F-Queue 中時(shí)或者已經(jīng)在隊(duì)列中的這一段時(shí)間內(nèi),只要重新與引用鏈上的任何一個(gè)對象建立聯(lián)系即可。

回收方法區(qū)(永久代)

在堆中,尤其是在新生代中,常規(guī)應(yīng)用進(jìn)行一次垃圾收集一般可以回收70%-90%的空間,但是在永久代垃圾回收率卻遠(yuǎn)低于此。

永久代垃圾回收主要負(fù)責(zé)兩部分內(nèi)容:

  1. 廢棄常量
    沒人用則進(jìn)行回收

  2. 回收無用類
    判斷一個(gè)常量是否需要回收比較簡單,但是回收一個(gè)類需要滿足下面三個(gè)條件。

    • 該類所有的實(shí)例已經(jīng)被回收,也就是Java堆中不存在該類的任何實(shí)例
    • 加載該類的類加載器已經(jīng)被回收
    • 該類對應(yīng)的java.lang.Class對象沒有在任何地方被使用,無法在任何地方通過反射調(diào)用該類的方法。

回收永久代的常用場景:

在大量使用反射,動態(tài)代理,CGLib等框架,動態(tài)生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機(jī)具備類卸載功能,以保證永久代不會發(fā)生內(nèi)存溢出。

垃圾收集算法

  1. 標(biāo)記--清楚算法

標(biāo)記過程前面已經(jīng)介紹過,一個(gè)對象需要經(jīng)歷兩次標(biāo)記的過程才能被確定為可回收的對象,清除時(shí)直接釋放原對象所占的內(nèi)存空間即可。

這樣做的問題也隨之而來,會帶來內(nèi)存碎片,這將導(dǎo)致如果突然加載一個(gè)大的對象將會導(dǎo)致內(nèi)存溢出的問題。

另外該算法還存在效率問題,標(biāo)記和清除兩個(gè)過程的效率都不夠高。

  1. 復(fù)制算法

復(fù)制算法為解決效率問題而生,它將內(nèi)存劃分為兩塊相等的空間,每次只使用其中一塊,當(dāng)這一塊內(nèi)存用完之后,就將這塊內(nèi)存中還存活的對象復(fù)制到另外一塊內(nèi)存中,再把該內(nèi)存塊中的內(nèi)存全部釋放掉。

該算法順手也把內(nèi)存碎片的問題給解決了,但是該算法付出的代價(jià)也很大--將內(nèi)存縮小了一半。

然而,很多商業(yè)虛擬機(jī)采用該算法回收新生代的對象,他們在使用該算法的時(shí)候,并不會按照一比一的比例去劃分內(nèi)存,而是按照8:1:1的比例將內(nèi)存劃分為3個(gè)部分,這三個(gè)部分分別對應(yīng)新生代的Eden,From Survivor,To Survivor區(qū)。

每次使用時(shí),只使用其中的Eden和From Survivor區(qū),也就是說每次只使用內(nèi)存的90%的容量。當(dāng)GC時(shí),將Eden和From Survivor中還存活的對象存放在To Survivor區(qū)中,當(dāng)To Survivor區(qū)中內(nèi)存不足以存放下所有新生代存活的對象時(shí),需要依賴其他內(nèi)存進(jìn)行分配擔(dān)保(這里指老年代)。

分配擔(dān)保--即新生代發(fā)生GC時(shí),虛擬機(jī)會先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間。

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

該算法是標(biāo)記過程與前兩種無異,但是后續(xù)步驟是讓所用存活對象都向一端移動,然后將存活的對象按地址大小進(jìn)行排列,最后將大于隊(duì)列末端的地址全部釋放掉。

該算法很適合對老年代進(jìn)行GC。

  1. 分代收集算法
    對新生代采用復(fù)制算法,對老年代采用標(biāo)記--整理算法。

HptSpot算法實(shí)現(xiàn)

  1. 枚舉根節(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è)檢查這里面的引用,必然耗費(fèi)很多時(shí)間。

枚舉根節(jié)點(diǎn)就是解決如何找到GC Roots的問題。

使用一組稱為OopMap的數(shù)據(jù)結(jié)構(gòu)來達(dá)到這個(gè)目的。

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

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