一、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ù)是存儲在運(yùn)行時常量池)、c.方法區(qū)中常量引用的對象,d.本地方法中JNI引用的對象,這些對象都存儲在java堆。

二、什么時候GC
2.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.可達(dá)性分析
目前 Java 虛擬機(jī)的主流垃圾回收器采取的是可達(dá)性分析算法。該算法的實質(zhì):將一系列 GC Roots 作為初始的存活對象合集(live set),然后從該合集出發(fā),探索所有能夠被該集合引用到的對象,并將其加入到該集合中,這個過程我們也稱之為標(biāo)記(mark)。最終,未被探索到的對象便是死亡的,是可以回收的。
從GC Roots開始向下搜索,搜索所走過的路徑稱為引用鏈。當(dāng)一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的不可達(dá)對象。如下圖所示,右側(cè)的對象是到GCRoot時不可達(dá)的,可以判定為可回收對象。

思考題:什么是GC Roots?GC Roots與GC對象的關(guān)系?
解答:由堆外指向堆內(nèi)的引用,一般而言,GC Roots 包括(但不限于)下列幾種,Java 方法棧楨中的局部變量、已加載類的靜態(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)行GC
GC算法是內(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)記的對象。

3.1.2 復(fù)制算法
復(fù)制算法是,將原有的內(nèi)存空間分為兩塊,每次只使用其中一塊,在垃圾回收時,將正在適用的內(nèi)存中存活對象復(fù)制到未使用的內(nèi)存塊,然后清除使用的內(nèi)存塊中所有的對象。

3.1.3 標(biāo)記壓縮算法
標(biāo)記壓縮算法是一種老年代的回收算法。
標(biāo)記階段:與標(biāo)記清除算法一致,對可達(dá)對象做一次標(biāo)記。
清理階段:為了避免內(nèi)存碎片產(chǎn)生,將所有的存活對象壓縮到內(nèi)存的一端。

四、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í)行這段代碼。