可達(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)記過程:
如果對象在經(jīng)歷一次可達(dá)性分析之后發(fā)現(xiàn)沒有與 GC Roots 相連接的引用鏈,那么他將會被第一次標(biāo)記。
如果這個(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)容:
廢棄常量
沒人用則進(jìn)行回收-
回收無用類
判斷一個(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)存溢出。
垃圾收集算法
- 標(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è)過程的效率都不夠高。
- 復(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ù)空間是否大于新生代所有對象總空間。
- 標(biāo)記--整理算法
該算法是標(biāo)記過程與前兩種無異,但是后續(xù)步驟是讓所用存活對象都向一端移動,然后將存活的對象按地址大小進(jìn)行排列,最后將大于隊(duì)列末端的地址全部釋放掉。
該算法很適合對老年代進(jìn)行GC。
- 分代收集算法
對新生代采用復(fù)制算法,對老年代采用標(biāo)記--整理算法。
HptSpot算法實(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è)檢查這里面的引用,必然耗費(fèi)很多時(shí)間。
枚舉根節(jié)點(diǎn)就是解決如何找到GC Roots的問題。
使用一組稱為OopMap的數(shù)據(jù)結(jié)構(gòu)來達(dá)到這個(gè)目的。