溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Java程序員必備技能內(nèi)存管理機(jī)——垃圾標(biāo)記

發(fā)布時(shí)間:2020-07-03 22:47:08 來(lái)源:網(wǎng)絡(luò) 閱讀:138 作者:wx5d9ed7c8443c3 欄目:編程語(yǔ)言

正文

1、怎么找到存活對(duì)象?

通過(guò)上篇文章我們知道,JVM創(chuàng)建對(duì)象時(shí)會(huì)通過(guò)某種方式從內(nèi)存中劃分一塊區(qū)域進(jìn)行分配。那么當(dāng)我們服務(wù)器源源不斷的接收請(qǐng)求的時(shí)候,就會(huì)頻繁的需要進(jìn)行內(nèi)存分配的操作,但是我們服務(wù)器的內(nèi)存確是非常有限的呢!所以對(duì)不再使用的內(nèi)存進(jìn)行回收再利用就成了JVM肩負(fù)的重任了! 那么,擺在JVM面前的問(wèn)題來(lái)了,怎么判斷哪些內(nèi)存不再使用了?怎么合理、高效的進(jìn)行回收操作?既然要回收,那第一步就是要找到需要回收的對(duì)象!

1.1、引用計(jì)數(shù)法

實(shí)現(xiàn)思路:給對(duì)象添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它,計(jì)數(shù)器加1。當(dāng)引用失效,計(jì)數(shù)器值減1。任何時(shí)刻計(jì)數(shù)器值為0,則認(rèn)為對(duì)象是不再被使用的。舉個(gè)小栗子,我們有一個(gè)People的類,People類有id和bestFriend的屬性。我們用People類來(lái)造兩個(gè)小人:

 People p1 = new People();
 People p2 = new People();

通過(guò)上篇文章的知識(shí)我們知道,當(dāng)方法執(zhí)行的時(shí)候,方法的局部變量表和堆的關(guān)系應(yīng)該是如下圖的(注意堆中對(duì)象頭中紅色括號(hào)內(nèi)的數(shù)字,就是引用計(jì)數(shù)器,這里只是舉栗,實(shí)際實(shí)現(xiàn)可能會(huì)有差異):

Java程序員必備技能內(nèi)存管理機(jī)——垃圾標(biāo)記

造出來(lái)的p1和p2兩個(gè)人,我想讓他們互為最好的朋友,于是代碼如下:

 People p1 = new People();
 People p2 = new People();
 p1.setBestFriend(p2);
 p2.setBestFriend(p1);

對(duì)應(yīng)的引用關(guān)系圖應(yīng)該如下(注意引用計(jì)數(shù)器值的變化):

Java程序員必備技能內(nèi)存管理機(jī)——垃圾標(biāo)記

然后我們?cè)僮鲆恍┨幚恚コ兞亢投阎袑?duì)象的引用關(guān)系。

 People p1 = new People();
 People p2 = new People();

 p1.setBestFriend(p2);
 p2.setBestFriend(p1);

 p1 = null;
 p2 = null;

這時(shí)候引用關(guān)系圖就變成如下了,由于p1和p2對(duì)象還相互引用著,所以引用計(jì)數(shù)器的值還為1。

Java程序員必備技能內(nèi)存管理機(jī)——垃圾標(biāo)記

優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,效率高。

缺點(diǎn):很難解決對(duì)象之間的相互循環(huán)引用。且開(kāi)銷較大,頻繁的引用變化會(huì)帶來(lái)大量的額外運(yùn)算。在談實(shí)現(xiàn)思路的時(shí)候有這樣一句話“任何時(shí)刻計(jì)數(shù)器值為0,則認(rèn)為對(duì)象是不再被使用的”。但是通過(guò)上面的例子我們可以看到,雖然對(duì)象已經(jīng)不再使用了,但計(jì)數(shù)器的值仍然是1,所以這兩個(gè)對(duì)象不會(huì)被標(biāo)記為垃圾。

現(xiàn)狀:主流的JVM都沒(méi)有選用引用計(jì)數(shù)法來(lái)管理內(nèi)存。

1.2、可達(dá)性分析

實(shí)現(xiàn)思路:通過(guò)GC Roots的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)向下搜索,搜索走過(guò)的路徑成為引用鏈,當(dāng)一個(gè)對(duì)象到GC Root沒(méi)有任何引用鏈相連時(shí),則證明對(duì)象是不可用的。如下圖,紅色的幾個(gè)對(duì)象由于沒(méi)有跟GC Root沒(méi)有任何引用鏈相連,所以會(huì)進(jìn)行標(biāo)記。

Java程序員必備技能內(nèi)存管理機(jī)——垃圾標(biāo)記

優(yōu)點(diǎn):可以很好的解決對(duì)象相互循環(huán)引用的問(wèn)題。

缺點(diǎn):實(shí)現(xiàn)比較復(fù)雜;需要分析大量數(shù)據(jù),消耗大量時(shí)間;

現(xiàn)狀:主流的JVM(如HotSpot)都選用可達(dá)性分析來(lái)管理內(nèi)存。

2、標(biāo)記死亡對(duì)象

通過(guò)可達(dá)性分析可以對(duì)需要回收的對(duì)象進(jìn)行標(biāo)記,是否標(biāo)記的對(duì)象一定會(huì)被回收呢?并不是呢!要真正宣告一個(gè)對(duì)象的死亡,至少要經(jīng)歷兩次的標(biāo)記過(guò)程!

2.1、第一次標(biāo)記

在可達(dá)性分析后發(fā)現(xiàn)到GC Roots沒(méi)有任何引用鏈相連時(shí),被第一次標(biāo)記。并且判斷此對(duì)象是否必要執(zhí)行finalize()方法!如果對(duì)象沒(méi)有覆蓋finalize()方法或者finalize()已經(jīng)被JVM調(diào)用過(guò),則這個(gè)對(duì)象就會(huì)認(rèn)為是垃圾,可以回收。對(duì)于覆蓋了finalize()方法,且finalize()方法沒(méi)有被JVM調(diào)用過(guò)時(shí),對(duì)象會(huì)被放入一個(gè)成為F-Queue的隊(duì)列中,等待著被觸發(fā)調(diào)用對(duì)象的finalize()方法。

2.2、第二次標(biāo)記

執(zhí)行完第一次的標(biāo)記后,GC將對(duì)F-Queue隊(duì)列中的對(duì)象進(jìn)行第二次小規(guī)模標(biāo)記。也就是執(zhí)行對(duì)象的finalize()方法!如果對(duì)象在其finalize()方法中重新與引用鏈上任何一個(gè)對(duì)象建立關(guān)聯(lián),第二次標(biāo)記時(shí)會(huì)將其移出"即將回收"的集合。如果對(duì)象沒(méi)有,也可以認(rèn)為對(duì)象已死,可以回收了。

finalize()方法是被第一次標(biāo)記對(duì)象的逃脫死亡的最后一次機(jī)會(huì)。在jvm中,一個(gè)對(duì)象的finalize()方法只會(huì)被系統(tǒng)調(diào)用一次,經(jīng)過(guò)finalize()方法逃脫死亡的對(duì)象,第二次不會(huì)再調(diào)用。由于該方法是在對(duì)象進(jìn)行回收的時(shí)候調(diào)用,所以可以在該方法中實(shí)現(xiàn)資源關(guān)閉的操作。但是,由于該方法執(zhí)行的時(shí)間是不確定的,甚至,在java程序不正常退出的情況下該方法都不一定會(huì)執(zhí)行!所以在正常情況下,盡量避免使用!如果需要"釋放資源",可以定義顯式的終止方法,并在"try-catch-finally"的finally{}塊中保證及時(shí)調(diào)用,如File相關(guān)類的close()方法。下面我們看一個(gè)在finalize中逃脫死亡的栗子吧:

public class GCDemo {
 public static GCDemo gcDemo = null;
 public static void main(String[] args) throws InterruptedException {
 gcDemo = new GCDemo();
 System.out.println("------------對(duì)象剛創(chuàng)建------------");
 if (gcDemo != null) {
 System.out.println("我還活得好好的!");
 } else {
 System.out.println("我死了!");
 }
 gcDemo = null;
 System.gc();
 System.out.println("------------對(duì)象第一次被回收后------------");
 Thread.sleep(500);// 由于finalize方法的調(diào)用時(shí)間不確定(F-Queue線程調(diào)用),所以休眠一會(huì)兒確保方法完成調(diào)用
 if (gcDemo != null) {
 System.out.println("我還活得好好的!");
 } else {
 System.out.println("我死了!");
 }
 gcDemo = null;
 System.gc();
 System.out.println("------------對(duì)象第二次被回收后------------");
 Thread.sleep(500);
 if (gcDemo != null) {
 System.out.println("我還活得好好的!");
 } else {
 System.out.println("我死了!");
 }
 // 后面無(wú)論多少次GC都不會(huì)再執(zhí)行對(duì)象的finalize方法
 }
 @Override
 protected void finalize() throws Throwable {
 super.finalize();
 System.out.println("execute method finalize()");
 gcDemo = this;
 }
}

執(zhí)行結(jié)果如下,具體就不多說(shuō)啦,不明白的就自己動(dòng)手去試試吧!

Java程序員必備技能內(nèi)存管理機(jī)——垃圾標(biāo)記

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

通過(guò)上面可達(dá)性分析我們了解了有哪些GC Root,了解了通過(guò)這些GC Root去搜尋并標(biāo)記對(duì)象是生存還是死亡的思路。但是具體的實(shí)現(xiàn)就是那張圖顯示的那么簡(jiǎn)單嗎?當(dāng)然不是,因?yàn)槲覀兊亩咽欠执占?,那GC Root連接的對(duì)象可能在新生代,也可能在老年代,新生代的對(duì)象可能會(huì)引用老年代的對(duì)象,老年代的對(duì)象也可能引用新生代。如果直接通過(guò)GC Root去搜尋,則每次都會(huì)遍歷整個(gè)堆,那分代收集就沒(méi)法實(shí)現(xiàn)了呢!并且,枚舉整個(gè)根節(jié)點(diǎn)的時(shí)候是需要線程停頓的(保證一致性,不能出現(xiàn)正在枚舉 GC Roots,而程序還在跑的情況,這會(huì)導(dǎo)致 GC Roots 不斷變化,產(chǎn)生數(shù)據(jù)不一致導(dǎo)致統(tǒng)計(jì)不準(zhǔn)確的情況),而枚舉根節(jié)點(diǎn)又比較耗時(shí),這在大并發(fā)高訪問(wèn)量情況下,分分鐘就會(huì)導(dǎo)致系統(tǒng)癱瘓!啥意思呢,下面一張圖感受一下:

Java程序員必備技能內(nèi)存管理機(jī)——垃圾標(biāo)記

如果是進(jìn)行根節(jié)點(diǎn)枚舉,我們先要全棧掃描,找到變量表中存放為reference類型的變量,然后找到堆中對(duì)應(yīng)的對(duì)象,最后遍歷對(duì)象的數(shù)據(jù)(如屬性等),找到對(duì)象數(shù)據(jù)中存放為指向其他reference的對(duì)象……這樣的開(kāi)銷無(wú)疑是非常大的!

為解決上述問(wèn)題,HotSpot 采用了一種 “準(zhǔn)確式GC” 的技術(shù),該技術(shù)主要功能就是讓虛擬機(jī)可以準(zhǔn)確的知道內(nèi)存中某個(gè)位置的數(shù)據(jù)類型是什么,比如某個(gè)內(nèi)存位置到底是一個(gè)整型的變量,還是對(duì)某個(gè)對(duì)象的reference,這樣在進(jìn)行 GC Roots枚舉時(shí),只需要枚舉reference類型的即可。那怎么讓虛擬機(jī)準(zhǔn)確的知道哪些位置存在的是reference類型數(shù)據(jù)呢?OopMap+RememberedSet!

OopMap記錄了棧上本地變量到堆上對(duì)象的引用關(guān)系,在GC發(fā)生時(shí),線程會(huì)運(yùn)行到最近的一個(gè)安全點(diǎn)停下來(lái),然后更新自己的OopMap,記下棧上哪些位置代表著引用。枚舉根節(jié)點(diǎn)時(shí),遞歸遍歷每個(gè)棧幀的OopMap,通過(guò)棧中記錄的被引用對(duì)象的內(nèi)存地址,即可找到這些對(duì)象( GC Roots )。這樣,OopMap就避免了全棧掃描,加快枚舉根節(jié)點(diǎn)的速度。

OopMap解決了枚舉根節(jié)點(diǎn)耗時(shí)的問(wèn)題,但是分代收集的問(wèn)題依然存在!這時(shí)候就需要另一利器了- RememberedSet。對(duì)于位于不同年代對(duì)象之間的引用關(guān)系,會(huì)在引用關(guān)系發(fā)生時(shí),在新生代邊上專門開(kāi)辟一塊空間記錄下來(lái),這就是RememberedSet!所以“新生代的 GC Roots ” + “ RememberedSet存儲(chǔ)的內(nèi)容”,才是新生代收集時(shí)真正的GC Roots(G1 收集器也使用了 RememberedSet 這種技術(shù))。

3.1、安全點(diǎn)

HotSpot在OopMap的幫助下可以快速且準(zhǔn)確的完成GC Roots枚舉,但是在運(yùn)行過(guò)程中,非常多的指令都會(huì)導(dǎo)致引用關(guān)系變化,如果為這些指令都生成對(duì)應(yīng)的OopMap,需要的空間成本太高。所以只在特定的位置記錄OopMap引用關(guān)系,這些位置稱為安全點(diǎn)(Safepoint)。如何在GC發(fā)生時(shí)讓所有線程(不包括JNI線程)運(yùn)行到其所在最近的安全點(diǎn)上再停頓下來(lái)?這里有兩種方案:

1、搶先式中斷:不需要線程的執(zhí)行代碼去主動(dòng)配合,當(dāng)發(fā)生GC時(shí),先強(qiáng)制中斷所有線程,然后如果發(fā)現(xiàn)某些線程未處于安全點(diǎn),那么將其喚醒,直至其到達(dá)安全點(diǎn)再次將其中斷。這樣一直等待所有線程都在安全點(diǎn)后開(kāi)始GC。

2、主動(dòng)式中斷:不強(qiáng)制中斷線程,只是簡(jiǎn)單地設(shè)置一個(gè)中斷標(biāo)記,各個(gè)線程在執(zhí)行時(shí)主動(dòng)輪詢這個(gè)標(biāo)記,一旦發(fā)現(xiàn)標(biāo)記被改變(出現(xiàn)中斷標(biāo)記)時(shí),就將自己中斷掛起。目前所有商用虛擬機(jī)全部采用主動(dòng)式中斷。

安全點(diǎn)既不能太少,以至于 GC 過(guò)程等待程序到達(dá)安全點(diǎn)的時(shí)間過(guò)長(zhǎng),也不能太多,以至于 GC 過(guò)程帶來(lái)的成本過(guò)高。安全點(diǎn)的選定基本上是以程序“是否具有讓程序長(zhǎng)時(shí)間執(zhí)行的特征”為標(biāo)準(zhǔn)進(jìn)行選定的,例如方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等,所以具有這些功能的指令才會(huì)產(chǎn)生安全點(diǎn)(在主動(dòng)式中斷中,輪詢標(biāo)志的地方和安全點(diǎn)是重合的,所以線程在遇到這些指令時(shí)都會(huì)去輪詢中斷標(biāo)志!)。

3.2、安全區(qū)域

使用安全點(diǎn)似乎已經(jīng)完美解決如何進(jìn)入GC的問(wèn)題了,但是GC發(fā)生的時(shí)候,某個(gè)線程正在睡覺(jué)(sleep),無(wú)法響應(yīng)JVM的中斷請(qǐng)求,這時(shí)候線程一旦醒來(lái)就會(huì)繼續(xù)執(zhí)行了,這會(huì)導(dǎo)致引用關(guān)系發(fā)生變化呢!所以需要安全區(qū)域的思路來(lái)解決這個(gè)問(wèn)題。線程執(zhí)行進(jìn)入安全區(qū)域,首先標(biāo)識(shí)自己已經(jīng)進(jìn)入安全區(qū)域。線程被喚醒離開(kāi)安全區(qū)域時(shí),其需要檢查系統(tǒng)是否已經(jīng)完成根節(jié)點(diǎn)枚舉(或整個(gè)GC)。如果已經(jīng)完成,就繼續(xù)執(zhí)行,否則必須等待,直到收到可以安全離開(kāi)Safe Region的信號(hào)通知!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI