JVM垃圾回收機(jī)制(Garbage Collection)

1. 概述

Java內(nèi)存區(qū)域里講了Java的內(nèi)存運(yùn)行時數(shù)據(jù)區(qū)域分為如下5個部分

  • 程序計數(shù)器(Program Counter)
  • 虛擬機(jī)棧(Virtual Machine Stack)
  • 本地方法棧(Native Method Stack)
  • 堆(Heap)
  • 方法區(qū)(Method Area)

其中前三個數(shù)據(jù)區(qū)域隨著線程的啟動而創(chuàng)建,終止而銷毀,這三個區(qū)域的內(nèi)存回收具有確定性,不需要過多考慮回收問題。所以JVM的垃圾回收機(jī)制的注意力就集中于堆和方法區(qū),其中對堆的GC性價比是最高的,一般可以回收70%~95%的空間。

2. GC過程

首先討論的是對堆的GC,在這之前我們應(yīng)該知道要進(jìn)行垃圾回收的步驟應(yīng)該是

  • 知道哪些對象需要回收?
  • 用什么方式去回收?

判斷對象的存活

針對第一個問題我們得確定堆中對象的“存活”,一個對象的“存活”其實就是能否通過任何途徑使用該對象,下面通過一段Code看下就明白:

public class Main{
    public static void main(String[] args){
        A a = new A();
        a = null;
    }
}

在這段Code里面,一開始創(chuàng)建一個A類型的對象,變量a持有這個對象的引用,接著a被賦值為null后。從此就無法通過任何變量來使用這個對象了,那么這個對象也就是所謂的“死亡”了,而GC的 就是這些對象。接下來有兩種方法可以找出堆中存活和死亡的對象。

引用計數(shù)法(Reference Counting)

給每一個對象添加一個引用計數(shù)器,每當(dāng)對象被引用,就對該對象的引用計數(shù)器加一,當(dāng)引用失效時引用計數(shù)器就減一。直到對象的引用計數(shù)器為0時該對象就是已死亡,可被GC。這種方法看起來簡單高效,但JVM卻沒有使用它來判斷對象的存活,原因是它很難解決對象之間相互引用的問題。還是來一段Code看下:

public class Main{
    public static void main(String[] args){
        A a = new A();
        B b = new B();

        a.ref = b;
        b.ref = a;

        a = null;
        b = null;
    }
}

在這段Code中,ab兩個引用最后都null,也就是無法通過它們來使用一開始創(chuàng)建的兩個對象,雖然這樣它們卻無法回收,原因是創(chuàng)建的兩個對象相互引用導(dǎo)致兩個對象的引用計數(shù)器都不為0。所以也就有了第二種方法(可達(dá)性分析)來解決這個問題。

可達(dá)性分析算法(Reachability Analysis)

把堆中所有對象當(dāng)成一幅有向圖中的所有點(diǎn),對象之間的引用構(gòu)成了點(diǎn)與點(diǎn)的之間的路徑。接著從一系列被稱為GC Roots(一些被引用的對象)的點(diǎn)出發(fā)遍歷整個圖,圖中所有可以到達(dá)的點(diǎn)都是存活的對象,而那些不可到達(dá)的點(diǎn)則為死亡對象,將被GC。
可充當(dāng)GC Roots的對象有下面幾種:

  • 虛擬機(jī)棧中棧幀中本地變量表中變量引用的對象
  • 本地方法棧中本地的方法引用的對象
  • 方法區(qū)中類靜態(tài)變量引用的對象
  • 方法區(qū)中常量引用的對象

垃圾回收算法

解決完第一個問題(判斷對象的存活)后,就可以去回收這些對象占用的內(nèi)存了,至于怎么回收這些內(nèi)存,有下面幾種算法:

標(biāo)記-清除算法(Mark-Sweep)

標(biāo)記-清除算法如同它的名字一樣,有標(biāo)記和清除兩個階段。其中的標(biāo)記階段就是上面說到的確定對象的存活階段,確定了要回收的對象后就回收死亡的對象,存活的對象留在原地。標(biāo)記清除算法是最基礎(chǔ)的回收算法,它有兩個缺點(diǎn):

  • 標(biāo)記和清除階段效率都不高
  • 清除之后內(nèi)存會產(chǎn)生大量不連續(xù)的碎片,導(dǎo)致分配大內(nèi)存對象困難


    標(biāo)記清除算法

復(fù)制算法(Copying)

復(fù)制算法將內(nèi)存分為大小相等的兩塊,每次只使用一塊,待這塊內(nèi)存用完,將這一塊上存活的對象復(fù)制到另一塊上,再把存在垃圾對象的那一塊占用的內(nèi)存一次清掉。這樣做效率高的原因是存活的對象遠(yuǎn)遠(yuǎn)少于死亡的對象,從而只需復(fù)制少量的存活對象。

復(fù)制算法

復(fù)制算法解決了標(biāo)記-清除算法的清除階段效率低的問題和碎片問題但卻使可用內(nèi)存減少一半。其實有個辦法可以解決這個問題:

IBM公司的專業(yè)研究表明新生代中的對象98%是“朝生夕死”的,所以并不用按照1:1來劃分空間,而是將內(nèi)存分為3塊。一塊80%大小的Eden空間和兩塊10%大小的Survivor空間,每次使用一塊Eden和一塊Survivor,當(dāng)需要回收時,將使用中的Eden和Survivor上的存活對象復(fù)制到另一塊Survivor上,最后直接清理使用過的Eden和Survivor的內(nèi)存空間。這樣就使得空間的利用率達(dá)到90%。但如果存活的對象超過10%的話,Survivor的空間就不夠用了,這時就需要依賴?yán)夏甏M(jìn)行分配擔(dān)保。

標(biāo)記整理算法(Mark-Compact)

相比于復(fù)制算法,標(biāo)記整理算法使用與適用于老年代這種對象存活率高的區(qū)域。標(biāo)記整理和標(biāo)記清除很相似,前面的標(biāo)記步驟都一樣,不一樣在標(biāo)記整理在清除前多做了整理步驟讓存活的對象向一端移動,最后在清除掉端邊界以外的內(nèi)存。


標(biāo)記整理算法

分代收集算法(Generational Collection)

因為現(xiàn)在的商用JVM的垃圾回收都采用分代收集算法,所以一般把堆內(nèi)存劃分為新生代和老年代。剛創(chuàng)建的對象存在于新生代中,當(dāng)有一些對象經(jīng)歷垃圾回收達(dá)到一定次數(shù)還存活下來的話,這些對象將進(jìn)入老年代,所以老年代里的對象每次GC存活率都很高。因此針對于新生代和老年代對象的不同存活率,可以分別采取不同的垃圾回收算法,對于對象存活率低的新生代采用復(fù)制算法,而對于對象存活率高的老年代采用標(biāo)記清除或標(biāo)記整理算法。

以上介紹的是關(guān)于堆中的GC,下面來說下方法區(qū)的GC。


方法區(qū)的GC

方法區(qū)在HotSpot虛擬機(jī)中是永久代,相比于堆中的新生代和老年代,永久代進(jìn)行垃圾回收的性價比更低。
方法區(qū)的垃圾回收主要回收廢棄常量和無用的類,其中常量來自于方法區(qū)的常量池,包括字面值常量和符號引用?;厥粘A扛厥斩阎袑ο蠓浅n愃?,以字面值常量為例,如果不存在其他對象引用該字面值常量,如果發(fā)生GC且有必要的話,該字面值常量會被回收。對于無用的類的判斷比較苛刻,必須同時滿足下列三個條件:

  • 該類的所以實例都被回收
  • 加載該類的類加載器已經(jīng)被回收
  • 該類對應(yīng)的Class對象沒有在任何地方被引用

不過也可以滿足了上面的三個條件也不進(jìn)行回收,可以通過設(shè)置虛擬機(jī)參數(shù)來控制回收。

3. 內(nèi)存分配策略

  1. 對象優(yōu)先在 Eden 分配
    對象優(yōu)先在新生代的 Eden 區(qū)分配,當(dāng) Eden 區(qū)空間不夠時,執(zhí)行Minor GC

  2. 大對象直接進(jìn)入老年代
    設(shè)置 -XX:PretenureSizeThreshold 參數(shù),大于該參數(shù)的值的對象直接在老年代分配,避免在 Eden 區(qū)和 Survivor 區(qū)之間的大量內(nèi)存復(fù)制

  3. 長期存活的對象進(jìn)入老年代
    對象頭的Mark word擁有一個存儲分代年齡字段,每經(jīng)歷一次 Minor GC 存活下來該年齡字段加1,直到該年齡超過 XX:MaxTenuringThreshold 設(shè)置的值(默認(rèn)15),則移動到老年代。

  4. 動態(tài)對象年齡判定
    若 Survivor 區(qū)中同年齡所有對象大小總和大于 Survivor 空間一半,則年齡大于等于該年齡的對象可以直接進(jìn)入老年代。

  5. 空間分配擔(dān)保
    在發(fā)生 Minor GC 之前,JVM 先檢查老年代最大可用連續(xù)空間是否大于新生代所有對象大小,成立的話 Minor GC 確認(rèn)是安全的,則進(jìn)行Minor GC;否則如果 HandlePromotionFailure 設(shè)置的值為true并且老年代最大可用連續(xù)空間大于歷次晉升到老年代對象的平均大小,則進(jìn)行 Minor GC,否則進(jìn)行 Full GC。

4. Minor GC 與 Full GC

觸發(fā)條件

  • Minor GC:當(dāng) Eden 區(qū)空間滿時,就將觸發(fā) Minor GC
  • Full GC:
    • 調(diào)用 System.gc() 大多情況下回觸發(fā)Full GC,通過 -XX:+ DisableExplicitGC 來禁止 RMI 調(diào)用System.gc()。
    • 老年代空間不足
    • 空間分配擔(dān)保失敗
    • JDK 1.7 及以前的永久代空間不足
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.什么是垃圾回收? 垃圾回收(Garbage Collection)是Java虛擬機(jī)(JVM)垃圾回收器提供...
    簡欲明心閱讀 90,396評論 17 311
  • JVM架構(gòu) 當(dāng)一個程序啟動之前,它的class會被類裝載器裝入方法區(qū)(Permanent區(qū)),執(zhí)行引擎讀取方法區(qū)的...
    cocohaifang閱讀 1,853評論 0 7
  • JVM筆記 JDK:Java、JVM、Java API類庫,是支持java程序開發(fā)的最小環(huán)境。JRE:Java A...
    一條小袍袍YoY閱讀 976評論 0 4
  • JVM內(nèi)存區(qū)域 JVM將其管理的內(nèi)存分為若干數(shù)據(jù)區(qū)域,這些數(shù)據(jù)區(qū)域分布情況如下圖所示: 程序計數(shù)器:一塊較小內(nèi)存區(qū)...
    luoxn28閱讀 784評論 0 0
  • 【徐水聽課雜感之下篇 把學(xué)生的發(fā)現(xiàn)都成為我的課堂資源———原來繪本可以這樣讀】 從沒有人是在讀書,每個人都在書本中...
    在水一方2008閱讀 193評論 0 0

友情鏈接更多精彩內(nèi)容