前言
談起JVM, 那么就不得不提垃圾收集(Garbage Collection 通常被稱為“GC”).
什么是垃圾收集呢?
想解答這個問題, 我們最好將問題拆解開
- 如何確定垃圾?
- 如何回收垃圾?
- 何時回收垃圾?
下面圍繞這三件事, 我們站在JVM層面梳理下垃圾收集的機(jī)制.
如何確定垃圾?
從JVM層面來看, 它管理的是生命周期內(nèi)的全部實例對象, 那么所謂的垃圾其實就是“無用的對象”.
那么它是如何確定“無用對象”的?
引用計數(shù)法
在 Java 中,引用和對象是有關(guān)聯(lián)的, 必須使用引用來操作對象.
People zs = new People();
zs.setName("張三");
zs是個引用, 和真正的對象“new People()”關(guān)聯(lián)
因此, 簡單的辦法是通過引用計數(shù)來判斷一個對象是否可以回收.
所以在JVM中, 每個對象都在對象頭結(jié)構(gòu)中維護(hù)了一個引用計數(shù)屬性
- 對象被引用一次時, 計數(shù)就加1
- 對象的引用被釋放時,計數(shù)就減1
- 對象的計數(shù)為0的時, 這個對象就可以被回收了
引用計數(shù)法聽著雖然簡單易懂, 判定效率也高.
但是,當(dāng)前主流的虛擬機(jī)都沒有采用這個算法來管理內(nèi)存,其中最主要的原因是它很難解決對象之間互相循環(huán)引用的問題.
循環(huán)引用問題
所謂對象之間互相循環(huán)引用,如下面代碼所示:
除了對象 objA 和 objB 相互引用著對方之外,這兩個對象之間再無任何引用.
但是它們因為互相引用對方,導(dǎo)致它們的引用計數(shù)器都不為 0,于是引用計數(shù)算法無法通知 GC 回收器回收他們.
PS: 實際上以下示例是能回收的, 因為JVM沒有采用引用計數(shù)法
public class ReferenceCountingGc {
public Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc obj1 = new ReferenceCountingGc();
ReferenceCountingGc obj2 = new ReferenceCountingGc();
obj1.instance = obj2;
obj2.instance = obj1;
obj1 = null;
obj2 = null;
}
}
可達(dá)性分析
為了解決引用計數(shù)法的循環(huán)引用問題, Java 使用了可達(dá)性分析的方法.
可達(dá)性分析就是通過一系列的稱為 “GC Roots”的對象作為起點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,節(jié)點(diǎn)所走過的路徑稱為引用鏈.
當(dāng)一個對象到 GC Roots 沒有任何引用鏈相連的話,則證明此對象是不可用的.
如下圖中的 Object 6 ~ Object 10 之間雖有引用關(guān)系,但它們到 GC Roots 不可達(dá), 因此為需要被回收的對象.
在Java中, GC Roots包括:
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象
- 本地方法棧(Native 方法)中引用的對象
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
- 所有被同步鎖持有的對象
要注意的是:
- 不可達(dá)對象不等價于可回收對象
- 不可達(dá)對象變?yōu)榭苫厥諏ο笾辽僖?jīng)過兩次標(biāo)記過程,兩次標(biāo)記后仍然是可回收對象,則將面臨回收
如何回收垃圾?
確定垃圾, 那么如何回收呢?
這就不得不談一系列的垃圾回收算法, 算法實現(xiàn)會因各個平臺虛擬機(jī)的差異而不同, 這里我們只談幾種主流的算法思想.
標(biāo)記-清除算法 (Mark-Sweep)
這是最基礎(chǔ)的收集算法, 分為標(biāo)記、清除兩個階段
主要算法思想就是
通過算法標(biāo)記出回收對象(舉例: hotspot使用可達(dá)性分析),進(jìn)而回收標(biāo)記的對象占用的空間
從示例圖不難看出, 該算法的最大缺陷就
- 內(nèi)存碎片化
后續(xù)碰到分配大對象時(連續(xù)的內(nèi)存空間), 必然導(dǎo)致內(nèi)存不夠從而觸發(fā)額外的GC.
另外就是標(biāo)記和清除兩個過程本身的效率都不高.
標(biāo)記-復(fù)制算法(copying)
復(fù)制算法算是Mark-Sweep算法的升級版, 主要就是為了解決Mark-Sweep算法“內(nèi)存碎片化”的問題.
該算法的主要思想如下
按內(nèi)存容量將內(nèi)存劃分為等大小的兩塊. 每次只使用其中一塊,當(dāng)這一塊內(nèi)存滿后將尚存活的對象復(fù)制到另一塊上去, 把已使用的內(nèi)存清掉.
這種算法雖然實現(xiàn)簡單,內(nèi)存效率高,不易產(chǎn)生碎片,但是存在兩個較嚴(yán)重問題
可用內(nèi)存被壓縮到了原來的一半
存活對象較多的話, Copying算法的效率較低
標(biāo)記-整理算法(Mark-Compact)
為了解決以上兩種算法的缺陷, 進(jìn)而提出了標(biāo)記整理算法.
算法的主要思想如下
分為標(biāo)記也拜、整理兩個階段
標(biāo)記階段和Mark-Sweep相同, 不同點(diǎn)是標(biāo)記后不會清理對象, 而是將存活對象移向內(nèi)存的一端.
然后清除端邊界外的對象.
分代收集算法
上面介紹了幾個算法都各有優(yōu)缺點(diǎn), 但沒有哪個是絕對優(yōu)勢的.
只能說每個算法都有各自的應(yīng)用場景.
而在JVM垃圾回收領(lǐng)域, 面對各種內(nèi)存回收的復(fù)雜場景, 顯然, 不可能存在一種算法就能達(dá)到最優(yōu)解.
此時聰明的開發(fā)者就提出了一種想法, 既然無法“一招通殺”, 那么, 我就“分而治之”.
通過一定規(guī)則把內(nèi)存區(qū)域劃成幾塊, 這樣某些小塊的內(nèi)存回收場景就存在某個“最優(yōu)解回收算法”, 每塊都是最優(yōu)解, 那么總體上不就是最優(yōu)解么?!
于是, 分代收集算法應(yīng)運(yùn)而生.
嚴(yán)格來說, 分代收集算法并不是個垃圾回收算法, 而是把對象按生命周期來進(jìn)行內(nèi)存劃分的思想.
該算法的主要思想如下
根據(jù)對象存活的不同生命周期, 將內(nèi)存劃分為幾塊不同的區(qū)域.
一般情況下將Java堆劃分為新生代和老年代
新生代的對象特點(diǎn)是大部分對象都是朝生夕死,生命周期很短, 每次垃圾回收時有大量對象需要被回收
老年代的對象特點(diǎn)是生命周期較長,每次垃圾回收時只有少量對象需要被回收
結(jié)合新生代崎弃、老年代的特點(diǎn), 于是適配了合適的垃圾回收算法
新生代與復(fù)制算法
目前大部分JVM 的 GC 對于新生代都采取 Copying 算法,
因為新生代每次垃圾回收都要回收大部分死亡對象,存活的對象少, 所以要復(fù)制的操作比較少.
這樣的特點(diǎn)剛好能發(fā)揮Copying 算法的效率.
新生代的劃分并沒有嚴(yán)格按Copying 算法的1:1劃分法, 而是將新生代劃分為一塊較大的 Eden區(qū)和兩個較小的 Survivor區(qū)(From區(qū), To區(qū))(一般也稱為S1和S2區(qū)),
默認(rèn)內(nèi)存占比為 Eden:S1:S2 是8:1:1
每次使用Eden區(qū)和其中的一塊 Survivor 區(qū),當(dāng)進(jìn)行垃圾回收時,將該兩塊區(qū)中還存活的對象復(fù)制到另一塊 Survivor區(qū)中.
老年代與標(biāo)記整理算法
老年代本身存放的對象都是熬過了一輪輪GC的, 都是“存活幾率”較高的, 老年代最終存放著大量的對象, 所以每次只需對少量死亡對象進(jìn)行回收, 因而采用 Mark-Compact 算法.
一次完整的GC過程如下
實例理解:
-
新New的對象一般出現(xiàn)在Eden區(qū)
- PS: 少數(shù)大對象(需要連續(xù)的內(nèi)存空間) 會直接進(jìn)老年代
- PS: Hotspot可配置: -XX:PretenureSizeThreshold=2m , 即2m以上的對象直接進(jìn)老年代
-
慢慢的Eden區(qū)滿了, 此時觸發(fā)一次GC, 將還存活的對象復(fù)制到某個空的S區(qū), 稱為S1區(qū)
- PS: S1和S2身份隨時互換, 只有空的我們稱為S1區(qū), 兩個S區(qū)必然有一個是空的
- PS: 也就是假設(shè)年輕代空間比例8:1:1
慢慢的S1區(qū)也滿了, 此時觸發(fā)GC, 已滿的S1區(qū)和Eden區(qū)還存活的對象
對象的內(nèi)存分配主要在新生代的 Eden區(qū)和 From區(qū), 少數(shù)情況(比如new了個大對象, 新生代放不下了)會直接分配到老生代
- 當(dāng)新生代的 Eden Space 和 From Space 空間不足時就會發(fā)生一次 GC揪利,進(jìn)行 GC 后雹嗦,Eden Space 和 From Space 區(qū)的存活對象會被挪到 To Space祭往,然后將 Eden Space 和 From Space 進(jìn)行清理挡爵。
- 如果 To Space 無法足夠存儲某個對象荡含,則將這個對象存儲到老生代咒唆。
- 在進(jìn)行 GC 后,使用的便是 Eden Space 和 To Space 了释液,如此反復(fù)循環(huán)全释。
- 當(dāng)對象在 Survivor 區(qū)躲過一次 GC 后,其年齡就會+1误债。默認(rèn)情況下年齡到達(dá) 15 的對象會被 移到老生代中浸船。
請關(guān)注我的訂閱號
參考
- 《深入理解JAVA虛擬機(jī):JVM高級特性與最佳實踐》