柚子快報(bào)邀請(qǐng)碼778899分享:java JVM垃圾判定算法
柚子快報(bào)邀請(qǐng)碼778899分享:java JVM垃圾判定算法
垃圾收集技術(shù)是Java的一堵高墻。Java堆內(nèi)存中存放著幾乎所有的對(duì)象實(shí)例,垃圾收集器在對(duì)堆內(nèi)存進(jìn)行回收前,第一件事情就是要確定這些對(duì)象中哪些還存活,哪些已經(jīng)死去(即不可能再被任何途徑使用的對(duì)象)。也就是判定垃圾。通常有兩種方法:
引用計(jì)數(shù)法
引用計(jì)數(shù)法(Reference Counting)的算法是:給每個(gè)對(duì)象添加一個(gè)引用計(jì)數(shù)器,有一個(gè)引用,計(jì)數(shù)器值加1;當(dāng)引用失效,計(jì)數(shù)器值減1;任何時(shí)刻計(jì)數(shù)器值為0的對(duì)象就是不可能再被使用的。 這種方法實(shí)現(xiàn)簡(jiǎn)單,判定效率也很高,但有個(gè)問(wèn)題:它很難解決對(duì)象之間相互循環(huán)引用的問(wèn)題。
package org.hbin.gc;
/**
* VM args: -XX:+PrintGC -XX:+PrintGCDetails
* @author Haley
* @version 1.0
* 2024/9/1
*/
public class ReferenceCountingGC {
private Object instance;
private byte[] array = new byte[1024 * 1024 * 5];
public static void main(String[] args) {
ReferenceCountingGC a = new ReferenceCountingGC();
ReferenceCountingGC b = new ReferenceCountingGC();
a.instance = b;
b.instance = a;
a = null;
b = null;
System.gc();
}
}
運(yùn)行上述代碼,并打印GC日志:
[GC (System.gc()) 12918K->616K(125952K), 0.0010921 secs]
[Full GC (System.gc()) 616K->459K(125952K), 0.0097270 secs]
從運(yùn)行結(jié)果可以看到,GC日志中包含12918K->616K,說(shuō)明并沒(méi)有因?yàn)檫@兩個(gè)對(duì)象互相引用而不回收,也從側(cè)面說(shuō)明虛擬機(jī)并不是通過(guò)引用計(jì)數(shù)法來(lái)判斷垃圾的。
根可達(dá)性分析算法
根可達(dá)性分析算法的基本思路:通過(guò)一系列稱為GC Roots的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索路徑稱為引用鏈(Reference Chain)。當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連(圖論:從GC Roots到這個(gè)對(duì)象不可達(dá))時(shí),則證明此對(duì)象是不可用的。 在Java中,可作為GC Roots的對(duì)象包括:
虛擬機(jī)棧(棧幀中的局部變量表)中引用的對(duì)象方法區(qū)中類靜態(tài)屬性引用的對(duì)象方法區(qū)中常量引用的對(duì)象本地方法棧中JNI(即native方法)引用的對(duì)象
JVM中GC Roots的構(gòu)成非常復(fù)雜,根據(jù)程序執(zhí)行的語(yǔ)義、語(yǔ)言特性的支持及JVM內(nèi)部?jī)?yōu)化實(shí)現(xiàn),可以劃分為Java根、JVM根和其他根。
Java根用于找到Java程序執(zhí)行時(shí)產(chǎn)生的對(duì)象,包括:
類元數(shù)據(jù)對(duì)象:利用類加載器來(lái)跟蹤Java程序運(yùn)行時(shí)加載的類元數(shù)據(jù)對(duì)象Java對(duì)象:通過(guò)線程棧幀跟蹤Java程序的活躍對(duì)象
JVM根主要指JVM為了運(yùn)行Java程序所產(chǎn)生的一些對(duì)象,這些對(duì)象可以簡(jiǎn)單地被認(rèn)為是全局對(duì)象。主要有:
Universe:Java程序運(yùn)行時(shí)需要一些全局對(duì)象,比如Java支持8種基本數(shù)據(jù)類型,這些基本類型的信息需要對(duì)象來(lái)描述(作為全局對(duì)象也是為了性能考慮),這些對(duì)象就存放在Universe中。Monitor:全局監(jiān)視器對(duì)象,對(duì)于Monitor對(duì)象主要是用于鎖相關(guān),可能存在只有Monitor對(duì)象引用到內(nèi)存空間的對(duì)象,所以Monitor是JVM的根之一。JNI:JVM執(zhí)行本地代碼時(shí)使用API產(chǎn)生的對(duì)象,例如通過(guò)JNI API在堆中創(chuàng)建對(duì)象,這些對(duì)象只在JNI API中使用,所以需要單獨(dú)管理這些對(duì)象。JVMTI:使用JVM提供的接口用于調(diào)試、分析Java程序。System Dictionary:JVM在設(shè)計(jì)類加載時(shí),對(duì)于基本的類,比如Java中經(jīng)常使用的基礎(chǔ)類,會(huì)通過(guò)系統(tǒng)加載器加載這些類,而這些類在運(yùn)行Java程序一直都需要,所以這些類被單獨(dú)加載,單獨(dú)標(biāo)記。Management:JVM提供的內(nèi)存管理API,用于JVM內(nèi)存的統(tǒng)計(jì)信息,在使用這些API時(shí)需要?jiǎng)?chuàng)建Java對(duì)象,所以需要標(biāo)記。AOT:在JDK9引入了提前編譯。在AOT的編譯過(guò)程中會(huì)把全局對(duì)象和編譯優(yōu)化的代碼對(duì)象放在可執(zhí)行文件中,當(dāng)執(zhí)行時(shí)會(huì)用到這些對(duì)象,所以在回收時(shí)需要標(biāo)記。
其他根主要有:
語(yǔ)言特性的弱引用JVM弱根,例如String.intern()產(chǎn)生的對(duì)象等。
這些根共同構(gòu)成了GC Roots集合,實(shí)際上根的確定和虛擬機(jī)運(yùn)行時(shí)密切相關(guān)。對(duì)于弱根的處理在不同的GC實(shí)現(xiàn)中也會(huì)有所不同。
當(dāng)前主流編程語(yǔ)言的垃圾收集器基本上都是依靠可達(dá)性分析算法來(lái)判定對(duì)象是否存活的,理論上該算法要求全過(guò)程都基于一個(gè)能保障一致性的快照中才能夠進(jìn)行分析,這意味著必須全程凍結(jié)用戶線程的運(yùn)行。要根節(jié)點(diǎn)枚舉這個(gè)步驟中,由于GC Roots相比起整個(gè)Java堆中全部的對(duì)象畢竟還是極少數(shù),且在各種優(yōu)化技巧(如OopMap)的加持下,它帶來(lái)的停頓已經(jīng)是非常短暫且相對(duì)固定(不隨堆容量而增長(zhǎng))。可從GC Roots再繼續(xù)往下遍歷,這一步驟的停頓時(shí)間就必定會(huì)與Java堆容量直接成正比例關(guān)系了:堆越大,存儲(chǔ)的對(duì)象越多,對(duì)象圖結(jié)構(gòu)越復(fù)雜,要標(biāo)記更多對(duì)象而產(chǎn)生的停頓時(shí)間自然就更長(zhǎng)。 標(biāo)記階段是所有追蹤式垃圾收集算法的共同特征,如果這個(gè)階段會(huì)隨著堆變大而等比例增加停頓時(shí)間,其影響就會(huì)涉及幾乎所有的垃圾收集器。同理,如果能削減這部分停頓時(shí)間的話,那收益也將會(huì)是系統(tǒng)性的。 想解決或者降低用戶線程的停頓,要先搞清楚為什么必須在一個(gè)能保障一致性的快照上才能進(jìn)行對(duì)象圖的遍歷?這里引入三色標(biāo)記(Tri-color Marking),把垃圾收集器遍歷對(duì)象過(guò)程中遇到的對(duì)象,按照是否訪問(wèn)過(guò)標(biāo)記成三種顏色:
白色:表示對(duì)象尚未被訪問(wèn)過(guò)。顯然在剛開始,所有的對(duì)象都是白色的;分析結(jié)束時(shí),仍然是白色的對(duì)象,即代表不可達(dá)。黑色:表示對(duì)象已經(jīng)被垃圾收集器訪問(wèn)過(guò),且這個(gè)對(duì)象的所有引用都已經(jīng)掃描過(guò)。黑色的對(duì)象代表已經(jīng)掃描過(guò),它是安全存活的,如果有其他對(duì)象引用指向了黑色對(duì)象,無(wú)須重新掃描一遍。黑色對(duì)象不可能直接指向某個(gè)白色對(duì)象,而不經(jīng)過(guò)灰色對(duì)象?;疑罕硎緦?duì)象已經(jīng)訪問(wèn)過(guò),但這個(gè)對(duì)象上至少存在一個(gè)引用還沒(méi)有被掃描過(guò)。
如果整個(gè)掃描過(guò)程中,用戶線程是凍結(jié)的,只有收集器線程在工作,那不會(huì)有任何問(wèn)題。但如果用戶線程與收集器線程是并發(fā)工作呢?收集器線程標(biāo)記過(guò)程中,用戶線程隨時(shí)可能修改任何引用關(guān)系,這可能出現(xiàn)兩種后果:
原本消亡的對(duì)象錯(cuò)誤標(biāo)記為存活:這不是好事兒,但可以容忍,只是產(chǎn)生一些浮動(dòng)垃圾,它們會(huì)逃過(guò)本次收集,下次收集再清理掉就好。原本存活的對(duì)象錯(cuò)誤標(biāo)記為消亡:這是非常致命的,程序會(huì)因此發(fā)生錯(cuò)誤。
整個(gè)掃描標(biāo)記過(guò)程和異常情況如下: Wilson于1994年在理論上證明:當(dāng)且僅當(dāng)以下兩個(gè)條件同時(shí)滿足時(shí),會(huì)產(chǎn)生“對(duì)象消失”的問(wèn)題,即原本應(yīng)該是黑色的對(duì)象被誤標(biāo)為白色:
賦值時(shí)插入了一條或多條從黑色對(duì)象到白色對(duì)象的新引用賦值時(shí)刪除了全部從灰色對(duì)象到該白色對(duì)象的直接或間接引用 因此,要解決這個(gè)問(wèn)題,只需要破壞這兩個(gè)條件中的任意一個(gè)即可。由此,分別產(chǎn)生了兩種解決方案:增量更新:Incremental Update。增量更新要破壞的是第一個(gè)條件,當(dāng)黑色對(duì)象插入新的指向白色對(duì)象的引用關(guān)系時(shí),就將這個(gè)新插入的引用記錄下來(lái),等并發(fā)掃描結(jié)束之后,再將這些記錄過(guò)的引用關(guān)系中的黑色對(duì)象為根,重新掃描一次。也簡(jiǎn)化理解:黑色對(duì)象一旦新插入了指向白色對(duì)象的引用之后,它就變回灰色對(duì)象了。原始快照:Snapshot At The Beginning, STAB。原始快照要破壞的是第二個(gè)條件,當(dāng)灰色對(duì)象要?jiǎng)h除指向白色對(duì)象的引用關(guān)系時(shí),就將這個(gè)要?jiǎng)h除的引用記錄下來(lái),在并發(fā)掃描結(jié)束之后,再將這些記錄過(guò)的引用關(guān)系中的灰色對(duì)象為根,重新掃描一次。也可以簡(jiǎn)化理解:無(wú)論引用關(guān)系刪除與否,都會(huì)按照剛剛開始掃描那一刻的對(duì)象圖快照來(lái)進(jìn)行搜索。 以上無(wú)論是對(duì)引用關(guān)系記錄的插入還是刪除,虛擬機(jī)的記錄操作都是通過(guò)寫屏障來(lái)實(shí)現(xiàn)的。實(shí)際上在并發(fā)標(biāo)記的時(shí)候?qū)?yīng)有兩種不同的實(shí)現(xiàn),分別是讀屏障和寫屏障。屏障技術(shù)是在讀或?qū)懖僮鲿r(shí)執(zhí)行一段代碼,其目的是調(diào)整對(duì)象的顏色從而保證正確性。但是讀操作遠(yuǎn)多于寫操作,所以讀屏障的效率一般低于寫屏障的效率。 在HotSpot虛擬機(jī)中,兩種解決方案都有實(shí)際應(yīng)用。例如,CMS是基于增量更新來(lái)做并發(fā)標(biāo)記的,G1、Shenandoah則是用原始快照來(lái)實(shí)現(xiàn)。
引用分類
在JDK1.2之前,引用的定義是:如果reference類型的數(shù)據(jù)中存儲(chǔ)的數(shù)值代表的是另外一塊內(nèi)存的起始地址,就稱這塊內(nèi)存代表著一個(gè)引用。 JDK1.2之后,Java對(duì)引用的概念進(jìn)行了擴(kuò)充,將引用分為四種:強(qiáng)引用、軟引用、弱引用、虛引用,這四種引用強(qiáng)度依次逐漸減弱。最后還有一個(gè)特殊的引用FinalReference。
強(qiáng)引用:Strong Reference,代碼中普遍存在的,類似Object obj = new Object()這類的引用。只要引用還存在,垃圾收集器永遠(yuǎn)不會(huì)回收掉這些對(duì)象。軟引用:Soft Reference,用來(lái)描述一些還有用但非必需的對(duì)象。在系統(tǒng)將要發(fā)生內(nèi)存溢出之前,將會(huì)把這些對(duì)象列進(jìn)回收范圍之中進(jìn)行二次回收。如果這次回收還不骨足夠的內(nèi)存,才會(huì)拋出內(nèi)存溢出異常。相關(guān)Java類:SoftReference弱引用:Weak Reference,也用來(lái)描述非必需對(duì)象,但它的強(qiáng)度比軟引用更弱一些。當(dāng)垃圾回收器工作時(shí),無(wú)論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉弱引用關(guān)聯(lián)的對(duì)象。相關(guān)Java類:WeakReference。虛引用:也稱幽靈引用或幻影引用,它是最弱的一種引用關(guān)系。虛引用不影響對(duì)象的生成時(shí)間,也無(wú)法通過(guò)虛引用來(lái)取得一個(gè)對(duì)象實(shí)例。設(shè)置虛引用的唯一目的是能在這個(gè)對(duì)象被垃圾回收器回收時(shí)收到一個(gè)系統(tǒng)通知。相關(guān)Java類:PhantomReference。 兩個(gè)常用子類:
sum.misc.Cleaner:用于DirectByteBuffer對(duì)象回收的時(shí)候?qū)τ诙淹鈨?nèi)存的回收。java.lang.ref.Cleaner:用于在被引用的對(duì)象回收的時(shí)候觸發(fā)一個(gè)動(dòng)作,在OpenJDK9中將完全替代Object.finalize()方法。 最終引用:FinalReference,包權(quán)限,開發(fā)者無(wú)法直接繼承擴(kuò)展。它只有一個(gè)子類Finalizer,并且由final修飾,無(wú)法繼承擴(kuò)展。由于構(gòu)造方法是私有的,所以只能由HotSpot VM通過(guò)調(diào)用register()方法將被引用的對(duì)象封裝為Finalizer對(duì)象。 在類加載過(guò)程中,如果當(dāng)前類重寫了finalize(),則其對(duì)象會(huì)被封裝為FinalReference對(duì)象,這樣FinalReference對(duì)象的referent字段就指向了當(dāng)前類的對(duì)象。
Finalizer對(duì)象鏈會(huì)保存全部的只存在FinalizerReference引用且沒(méi)有執(zhí)行過(guò)finalize()方法的Finalizer對(duì)象,防止Finalizer對(duì)象在其引用的對(duì)象之前被GC回收。在GC過(guò)程中,如果發(fā)現(xiàn)referent對(duì)象不可達(dá),則Finalizer對(duì)象會(huì)添加到queue列表中,所有在queue隊(duì)列中的對(duì)象都會(huì)調(diào)用finalize()方法。
柚子快報(bào)邀請(qǐng)碼778899分享:java JVM垃圾判定算法
推薦鏈接
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場(chǎng)。
轉(zhuǎn)載請(qǐng)注明,如有侵權(quán),聯(lián)系刪除。