《Java 虛擬機(jī)原理》5.1 GC垃圾收集及案例分析
時間:2023-06-27 15:45:01 | 來源:網(wǎng)站運營
時間:2023-06-27 15:45:01 來源:網(wǎng)站運營
《Java 虛擬機(jī)原理》5.1 GC垃圾收集及案例分析:
一、GC什么對象GC的對象是沒有存活的對象,判斷沒有存活的對象有兩種常用方法:
引用計數(shù)和可
達(dá)性分析。
1.1 java的GCRoots引用對象在 Java 虛擬機(jī)的語境下,垃圾指的是
死亡的對象所占據(jù)的堆空間。
a. 虛擬機(jī)棧中引用的對象。
b. 方法區(qū)中靜態(tài)屬性引用的對象。
c.方法區(qū)中常量引用的對象。
d.本地方法中JNI引用的對象。
說明:當(dāng)前對象到GCRoots中不可達(dá)時候,即會滿足被垃圾回收的可能。這些對象但不是就非死不可,此時只能宣判它們存在于一種“緩刑”的階段,要真正的宣告一個對象死亡。至少要經(jīng)歷兩次標(biāo)記:
第一次:對象可達(dá)性分析之后,發(fā)現(xiàn)沒有與GCRoots相連接,此時會被第一次標(biāo)記并篩選。
第二次:對象沒有覆蓋finalize()方法,或者finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過,此時會被認(rèn)定為沒必要執(zhí)行。
1.2 結(jié)合GC對象回顧java虛擬機(jī)內(nèi)存說明:a. 虛擬機(jī)棧中引用的對象、b. 方法區(qū)中靜態(tài)屬性引用的對象(b.1基本類型數(shù)據(jù)是存儲在運行時常量池)、c.方法區(qū)中常量引用的對象,d.本地方法中JNI引用的對象,這些對象都存儲在java堆。
圖1 GC對象在java虛擬機(jī)內(nèi)存圖
二、什么時候GC2.1 判斷沒有存活的對象有兩種常用方法如何辨別一個對象的存亡是關(guān)鍵問題。
1.
引用計數(shù)每個對象有一個引用計數(shù)屬性,新增一個引用時計數(shù)加1,引用釋放時計數(shù)減1,計數(shù)為0時可以回收。此方法簡單,無法解決對象相互循環(huán)引用的問題。
優(yōu)點:實現(xiàn)簡單,判定效率高效,被actionscript3和python中廣泛應(yīng)用。
缺點:無法解決對象之間的循環(huán)引用問題。
圖2 循環(huán)引用場景
2.
可達(dá)性分析目前 Java 虛擬機(jī)的主流垃圾回收器采取的是
可達(dá)性分析算法。該算法的實質(zhì):將一系列 GC Roots 作為初始的存活對象合集(live set),然后從該合集出發(fā),探索所有能夠被該集合引用到的對象,并將其加入到該集合中,這個過程我們也稱之為
標(biāo)記(mark)。最終,
未被探索到的對象便是死亡的,是可以回收的。
從GC Roots開始向下搜索,搜索所走過的路徑稱為引用鏈。當(dāng)一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的不可達(dá)對象。如下圖所示,右側(cè)的對象是到GCRoot時不可達(dá)的,可以判定為可回收對象。
圖3 可達(dá)性分析
思考題:
什么是GC Roots?GC Roots與GC對象的關(guān)系?解答:
由堆外指向堆內(nèi)的引用,一般而言,GC Roots 包括(但不限于)下列幾種,J
ava 方法棧楨中的局部變量、
已加載類的靜態(tài)變量、
JNI handles、
已啟動且未停止的 Java 線程。因此,
GC Roots是GC對象的引用。
可達(dá)性分析法的問題:在多線程環(huán)境下,其他線程可能會更新已經(jīng)訪問過的對象中的引用,從而造成誤報(將引用設(shè)置為 null)或者漏報(將引用設(shè)置為未被訪問過的對象)。誤報使得Java 虛擬機(jī)損失該次垃圾回收的機(jī)會。漏報則比較麻煩,因為垃圾回收器可能回收事實上仍被引用的對象內(nèi)存。一旦從原引用訪問已經(jīng)被回收了的對象,則很有可能會直接導(dǎo)致 Java 虛擬機(jī)崩潰。
2.2 觸發(fā)GC的動作及時機(jī)(1)動作:程序調(diào)用
System.gc時可以觸發(fā)。
(2)時機(jī):
系統(tǒng)自身來決定GC觸發(fā)的時機(jī)根據(jù)Eden區(qū)和From Space區(qū)的內(nèi)存大小來決定,當(dāng)內(nèi)存大小不足時,則會啟動GC線程并停止應(yīng)用線程,GC又分為 Minor GC 和 Full GC。
Minor GC觸發(fā)條件:① 當(dāng) Eden 區(qū)滿時,觸發(fā) Minor GC。
② 當(dāng) FromSuv 或者 ToSuv 區(qū)滿時,觸發(fā) Minor GC。
Full GC觸發(fā)條件: ① 調(diào)用System.gc時,系統(tǒng)建議執(zhí)行Full GC,但是不必然執(zhí)行
② Heap 的老年區(qū)空間不足
③ Metaspace 空間不足
④ 通過Minor GC后進(jìn)入老年代的平均大小大于老年代的可用內(nèi)存
⑤ 由Eden區(qū)、From Space區(qū)向To Space區(qū)復(fù)制時,對象大小大于To Space可用內(nèi)存,則把該對象轉(zhuǎn)存到老年代,且老年代的可用內(nèi)存小于該對象大小
三、如何進(jìn)行GCGC算法是內(nèi)存回收的理論方法,而GC垃圾收集器則是是內(nèi)存回收的具體實現(xiàn)。下面的內(nèi)容先講GC常用算法。
3.1 GC算法理論基礎(chǔ)GC算法是內(nèi)存回收的理論方法。GC常用算法理論有:
標(biāo)記-清除算法,
標(biāo)記-壓縮算法,
復(fù)制算法,
分代收集算法。即回收垃圾對象的內(nèi)存共有三種方式,分別為:會造成內(nèi)存碎片的清除、性能開銷較大的壓縮、以及堆使用效率較低的復(fù)制。目前主流的JVM(HotSpot)采用的是分代收集算法。
3.1.1 標(biāo)記清除法標(biāo)記清除法是垃圾回收算法的思想基礎(chǔ)。標(biāo)記清除算法將垃圾分為兩個階段:標(biāo)記階段和清除階段。
標(biāo)記階段:通過根節(jié)點,標(biāo)記所有從根節(jié)點開始的可達(dá)對象,未標(biāo)記過的對象就是未被引用的垃圾對象。
清除階段:清除所有未被標(biāo)記的對象。
圖4 標(biāo)記清除算法
3.1.2 復(fù)制算法復(fù)制算法是,將原有的內(nèi)存空間分為兩塊,每次只使用其中一塊,在垃圾回收時,將正在適用的內(nèi)存中存活對象復(fù)制到未使用的內(nèi)存塊,然后清除使用的內(nèi)存塊中所有的對象。
圖5 復(fù)制算法
3.1.3 標(biāo)記壓縮算法標(biāo)記壓縮算法是一種
老年代的回收算法。
標(biāo)記階段:與標(biāo)記清除算法一致,對可達(dá)對象做一次標(biāo)記。
清理階段:為了避免內(nèi)存碎片產(chǎn)生,將所有的存活對象壓縮到內(nèi)存的一端。
圖6 標(biāo)記壓縮算法
四、Java虛擬機(jī)的堆劃分Java 虛擬機(jī)將堆劃分為
新生代和
老年代。其中,新生代又被劃分為
Eden 區(qū),以及兩個大小相同的 Survivor 區(qū)即
FromSuv和
ToSuv。
當(dāng)調(diào)用
new 指令時,java虛擬機(jī)在Eden區(qū)中劃出一塊作為存儲對象的內(nèi)存。由于堆空間是線程共享的,因此直接在Eden區(qū)是需要進(jìn)行
同步的。new 指令,便可以直接通過指針加法(bump the pointer)來實現(xiàn),即把指向空余內(nèi)存位置的指針加上所請求的字節(jié)數(shù)。
問題1:兩個線程同時new Object1對象,則堆如何劃分內(nèi)存?
解答:由于堆內(nèi)存是線程共享的,同步為兩個線程分別劃分object1的內(nèi)存空間,即有2個object1對象。該技術(shù)被稱為
TLAB(
Thread Local Allocation Buffer,對應(yīng)虛擬機(jī)參數(shù) -XX:+UseTLAB,默認(rèn)開啟)。
問題2:當(dāng) Eden 區(qū)的空間耗盡了怎么辦?
解答:這個時候Java虛擬機(jī)會觸發(fā)一次
Minor GC,來收集新生代的垃圾。存活下來的對象,則會被送到
Survivor區(qū)。
問題3:新生代的兩個Survivor 區(qū),即
FromSuv和
ToSuv有什么用處?
解答:當(dāng)Minor GC時,Eden和FromSuv中的存活對象會被復(fù)制到ToSuv中,然后交換FromSuv和ToSuv指針,以保證下一次Minor GC時,ToSuv還是空的。滿足兩種情況之一,可以使對象移動到老年代:
1. Minor GC,存活對象從FromSuv復(fù)制到ToSuv,其對象的age+1,當(dāng)超過
(默認(rèn)值)15的時候,轉(zhuǎn)移到老年代;
2. 動態(tài)對象,
如果survivor空間中相同年齡所有的對象大小總和,大于survivor空間的一半,則年級大于或等于該年級的對象就可以直接進(jìn)入老年代。
注意:
Minor GC只針對新生代進(jìn)行垃圾回收,所以在枚舉 GC Roots 的時候,需要
考慮從老年代到新生代的引用。為了避免掃描整個老年代,Java 虛擬機(jī)引入
卡表(Card Table)的技術(shù),大致地
標(biāo)出可能存在老年代到新生代引用的內(nèi)存區(qū)域。
五、GC案例分析從一個
object1分析該對象在分代垃圾回收算法中的
回收軌跡。
Minor GC是指發(fā)生在新生代的GC,因為Java對象大多是朝生夕滅,所以Minor GC非常頻繁,一般回收速度也比較快;
Full GC是指發(fā)生在老年代的GC,出現(xiàn)Full GC一般會伴隨至少一次的Minor GC,其速度一般比Minor GC慢10倍以上。
步驟1:實例化object1,出生于新生代的
Eden區(qū)域;
步驟2:
Minor GC,object1移動到新生代的
Fromsuv區(qū)域,object1還存活。
步驟3:
Minor GC,通過復(fù)制算法將object1移動到新生代的
ToSuv區(qū)域,同時object1的年齡age+1,object1 依然存活;
步驟4:
Minor GC,在新生代的survivor區(qū)域中,與object1同齡的對象并沒有達(dá)到survivor的一半。因此,通過復(fù)制算法將
FromSuv和ToSuv 區(qū)域進(jìn)行互換,object1對象被移動到了新生代的ToSuv,object1 依然存活;
步驟5:
Minor GC,此時survivor中和object1同齡的對象已經(jīng)達(dá)到
survivor的一半以上,object1被移動到了
老年代區(qū)域,object1 依然存活。
滿足兩種情況之一,都可以使object1對象移動到老年代:
1. Minor GC,存活于survivor 區(qū)域的object1對象的age+1,當(dāng)超過(默認(rèn)值)15的時候,轉(zhuǎn)移到老年代;
注意:minor GC下,步驟2/3/4中的移動/復(fù)制全部Tosuv/Fromsuv區(qū)域的對象。2. 動態(tài)對象,
如果survivor空間中相同年齡所有的對象大小總和,大于survivor空間的一半,則年級大于或等于該年級的對象就可以直接進(jìn)入老年代。
步驟6:
Full GC會觸發(fā)stop the world。object1存活一段時間后,此時GC Roots不可達(dá)object1,而且此時老年代空間比率已經(jīng)超過了閾值,觸發(fā)了Full GC,此時object1被回收。
注意:object1 被回收的必要條件是 object1
不可達(dá)(GC Roots),即 object1 的引用是
弱引用。
以上的步驟采用
分代垃圾收集的思想,描述object1對象從存活到死亡的過程。
新生代:采用復(fù)制算法,老年代:采用標(biāo)記-清除算法或者標(biāo)記-整理算法。
stop the world是一種簡單除暴的方式,即停止其他非垃圾回收線程的工作,直到完成垃圾回收。Java 虛擬機(jī)中的 stop the world 是通過
安全點(safepoint)機(jī)制來實現(xiàn)的。當(dāng) Java 虛擬機(jī)收到 Stop-the-world 請求,它便會等待所有的線程都到達(dá)安全點,才允許請求 stop the world 的線程進(jìn)行獨占的工作。安全點的初始目的并不是讓其他線程停下,而是找到一個
穩(wěn)定的執(zhí)行狀態(tài)。例如,
java執(zhí)行某個JNI本地方法時,不訪問Java對象、調(diào)用Java方法、返回至原Java方法,則Java虛擬機(jī)的堆棧不會發(fā)生改變,所以這段代碼可以作為安全點。因此,Java虛擬機(jī)在這個安全點,可以同時進(jìn)行垃圾回收和執(zhí)行這段代碼。