深入理解 JVM 02 | 垃圾收集

垃圾收集

垃圾收集主要是針對堆和方法區(qū)進行。程序計數(shù)器、虛擬機棧和本地方法棧這三個區(qū)域?qū)儆诰€程私有的,只存在于線程的生命周期內(nèi),線程結(jié)束之后就會消失,因此不需要對這三個區(qū)域進行垃圾回收。

判斷一個對象是否可被回收

1. 引用計數(shù)算法

為對象添加一個引用計數(shù)器,當(dāng)對象增加一個引用時計數(shù)器加 1,引用失效時計數(shù)器減 1。引用計數(shù)為 0 的對象可被回收。

在兩個對象出現(xiàn)循環(huán)引用的情況下,此時引用計數(shù)器永遠不為 0,導(dǎo)致無法對它們進行回收。正是因為循環(huán)引用的存在,因此 Java 虛擬機不使用引用計數(shù)算法。

2. 可達性分析算法

以 GC Roots 為起始點進行搜索,可達的對象都是存活的,不可達的對象可被回收。

Java 虛擬機使用該算法來判斷對象是否可被回收,GC Roots 一般包含以下內(nèi)容:

  • 虛擬機棧中局部變量表中引用的對象
  • 本地方法棧中 JNI 中引用的對象
  • 方法區(qū)中類靜態(tài)屬性引用的對象
  • 方法區(qū)中的常量引用的對象

3. 方法區(qū)的回收

因為方法區(qū)主要存放永久代對象,而永久代對象的回收率比新生代低很多,所以在方法區(qū)上進行回收性價比不高。

主要是對常量池的回收和對類的卸載。

為了避免內(nèi)存溢出,在大量使用反射和動態(tài)代理的場景都需要虛擬機具備類卸載功能。

4. finalize()

類似 C++ 的析構(gòu)函數(shù),用于關(guān)閉外部資源。但是 try-finally 等方式可以做得更好,并且該方法運行代價很高,不確定性大,無法保證各個對象的調(diào)用順序,因此最好不要使用。

當(dāng)一個對象可被回收時,如果需要執(zhí)行該對象的 finalize() 方法,那么就有可能在該方法中讓對象重新被引用,從而實現(xiàn)自救。自救只能進行一次,如果回收的對象之前調(diào)用了 finalize() 方法自救,后面回收時不會再調(diào)用該方法。也就是說每一個對象的finalize()(從Object繼承的方法)都只會被系統(tǒng)自動調(diào)用一次,如果對象面臨下一次回收,它的 finalize() 不會被再次執(zhí)行

package com.reference.test;

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }
    public static void helpGC() throws Throwable {
        // 在這里這個FinalizeEscapeGC對象有一個強引用SAVE_HOOK指向它, 如果不設(shè)置為null,垃圾回收器將不會回收該對象. 主動設(shè)置為null, 可以幫助垃圾收集器回收被引用的對象
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            System.out.println("yes, i am still alive.");
        } else {
            System.out.println("no, i am dead.");
        }
    }
    
    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();
        helpGC(); // 第一次執(zhí)行了finalize自救
        helpGC(); // finalize執(zhí)行過了一次便不再執(zhí)行了
    }
}
finalize method executed
yes, i am still alive.
no, i am dead.

引用類型

無論是通過引用計數(shù)算法判斷對象的引用數(shù)量,還是通過可達性分析算法判斷對象是否可達,判定對象是否可被回收都與引用有關(guān)。

Java 提供了四種強度不同的引用類型。

1. 強引用

被強引用關(guān)聯(lián)的對象不會被回收。

使用 new 一個新對象的方式來創(chuàng)建強引用。

Object obj = new Object();

2. 軟引用

被軟引用關(guān)聯(lián)的對象只有在內(nèi)存不夠的情況下才會被回收。

使用 SoftReference 類來創(chuàng)建軟引用。

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使對象只被軟引用關(guān)聯(lián)

用途:可用來實現(xiàn)內(nèi)存敏感的高速緩存

3. 弱引用

被弱引用關(guān)聯(lián)的對象一定會被回收,也就是說它只能存活到下一次垃圾回收發(fā)生之前。

使用 WeakReference 類來創(chuàng)建弱引用。

Copy to Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;

用途:ThreadLocalMap 中 Entry 繼承自 WeakReference<ThreadLocal<?>>

4. 虛引用

又稱為幽靈引用或者幻影引用,一個對象是否有虛引用的存在,不會對其生存時間造成影響,也無法通過虛引用得到一個對象。

為一個對象設(shè)置虛引用的唯一目的是能在這個對象被回收時收到一個系統(tǒng)通知。

使用 PhantomReference 來創(chuàng)建虛引用。

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;

垃圾收集算法

1. 標(biāo)記 - 清除

img

在標(biāo)記階段,程序會檢查每個對象是否為活動對象,如果是活動對象,則程序會在對象頭部打上標(biāo)記。

在清除階段,會進行對象回收并取消標(biāo)志位,另外,還會判斷回收后的分塊與前一個空閑分塊是否連續(xù),若連續(xù),會合并這兩個分塊?;厥諏ο缶褪前褜ο笞鳛榉謮K,連接到被稱為 “空閑鏈表” 的單向鏈表,之后進行分配時只需要遍歷這個空閑鏈表,就可以找到分塊。

在分配時,程序會搜索空閑鏈表尋找空間大于等于新對象大小 size 的塊 block。如果它找到的塊等于 size,會直接返回這個分塊;如果找到的塊大于 size,會將塊分割成大小為 size 與 (block - size) 的兩部分,返回大小為 size 的分塊,并把大小為 (block - size) 的塊返回給空閑鏈表。

不足:

  • 標(biāo)記和清除過程效率都不高;
  • 會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,導(dǎo)致無法給大對象分配內(nèi)存。

2. 標(biāo)記 - 整理

img

讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存。

優(yōu)點:

  • 不會產(chǎn)生內(nèi)存碎片

不足:

  • 需要移動大量對象,處理效率比較低。

3. 復(fù)制

img

將內(nèi)存劃分為大小相等的兩塊,每次只使用其中一塊,當(dāng)這一塊內(nèi)存用完了就將還存活的對象復(fù)制到另一塊上面,然后再把使用過的內(nèi)存空間進行一次清理。

主要不足是只使用了內(nèi)存的一半。

現(xiàn)在的商業(yè)虛擬機都采用這種收集算法回收新生代,但是并不是劃分為大小相等的兩塊,而是一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。在回收時,將 Eden 和 Survivor 中還存活著的對象全部復(fù)制到另一塊 Survivor 上,最后清理 Eden 和使用過的那一塊 Survivor。

HotSpot 虛擬機的 Eden 和 Survivor 大小比例默認為 8:1,保證了內(nèi)存的利用率達到 90%。如果每次回收有多于 10% 的對象存活,那么一塊 Survivor 就不夠用了,此時需要依賴于老年代進行空間分配擔(dān)保,也就是借用老年代的空間存儲放不下的對象。

4. 分代收集

現(xiàn)在的商業(yè)虛擬機采用分代收集算法,它根據(jù)對象存活周期將內(nèi)存劃分為幾塊,不同塊采用適當(dāng)?shù)氖占惴ā?/p>

一般將堆分為新生代和老年代。

新生代和老生代大小之比默認為 3:8(該值可以通過參數(shù) –XX:NewRatio 來指定,-Xmn 是設(shè)置新生代的大小)

新生代 Eden 和 Survivor 大小比例默認為 8:1 ( 可以通過參數(shù) –XX:SurvivorRatio 來設(shè)定 )

  • 新生代使用:復(fù)制算法
  • 老年代使用:標(biāo)記 - 清除 或者 標(biāo)記 - 整理 算法

垃圾收集器

Serial 收集器

Serial 翻譯為串行,也就是說它以串行的方式執(zhí)行。

它是單線程的收集器,只會使用一個線程進行垃圾收集工作。

它的優(yōu)點是簡單高效,在單個 CPU 環(huán)境下,由于沒有線程交互的開銷,因此擁有最高的單線程收集效率。

它是 Client 場景下的默認新生代收集器,因為在該場景下內(nèi)存一般來說不會很大。它收集一兩百兆垃圾的停頓時間可以控制在一百多毫秒以內(nèi),只要不是太頻繁,這點停頓時間是可以接受的。

ParNew 收集器

它是 Serial 收集器的多線程版本。

它是 Server 場景下默認的新生代收集器,除了性能原因外,主要是因為除了 Serial 收集器,只有它能與 CMS 收集器配合使用。

CMS 收集器

CMS(Concurrent Mark Sweep),Mark Sweep 指的是標(biāo)記 - 清除算法

G1 收集器

G1(Garbage-First),它是一款面向服務(wù)端應(yīng)用的垃圾收集器,在多 CPU 和大內(nèi)存的場景下有很好的性能。HotSpot 開發(fā)團隊賦予它的使命是未來可以替換掉 CMS 收集器。

堆被分為新生代和老年代,其它收集器進行收集的范圍都是整個新生代或者老年代,而 G1 可以直接對新生代和老年代一起回收。

內(nèi)存分配與回收策略

Minor GC 和 Full GC

  • Minor GC:回收新生代,因為新生代對象存活時間很短,因此 Minor GC 會頻繁執(zhí)行,執(zhí)行的速度一般也會比較快。
  • Full GC:回收老年代和新生代,老年代對象其存活時間長,因此 Full GC 很少執(zhí)行,執(zhí)行速度會比 Minor GC 慢很多。

內(nèi)存分配策略

1. 對象優(yōu)先在 Eden 分配

大多數(shù)情況下,對象在新生代 Eden 上分配,當(dāng) Eden 空間不夠時,發(fā)起 Minor GC。

2. 大對象直接進入老年代

大對象是指需要連續(xù)內(nèi)存空間的對象,最典型的大對象是那種很長的字符串以及數(shù)組。

經(jīng)常出現(xiàn)大對象會提前觸發(fā)垃圾收集以獲取足夠的連續(xù)空間分配給大對象。

-XX:PretenureSizeThreshold,大于此值的對象直接在老年代分配,避免在 Eden 和 Survivor 之間的大量內(nèi)存復(fù)制。

3. 長期存活的對象進入老年代

為對象定義年齡計數(shù)器,對象在 Eden 出生并經(jīng)過 Minor GC 依然存活,將移動到 Survivor 中,年齡就增加 1 歲,增加到一定年齡則移動到老年代中。

-XX:MaxTenuringThreshold 用來定義年齡的閾值。

4. 動態(tài)對象年齡判定

虛擬機并不是永遠要求對象的年齡必須達到 MaxTenuringThreshold 才能晉升老年代,如果在 Survivor 中相同年齡所有對象大小的總和大于 Survivor 空間的一半,則年齡大于或等于該年齡的對象可以直接進入老年代,無需等到 MaxTenuringThreshold 中要求的年齡。

5. 空間分配擔(dān)保

在發(fā)生 Minor GC 之前,虛擬機先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間,如果條件成立的話,那么 Minor GC 可以確認是安全的。

如果不成立的話虛擬機會查看 HandlePromotionFailure 的值是否允許擔(dān)保失敗,如果允許那么就會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將嘗試著進行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允許冒險,那么就要進行一次 Full GC。

Full GC 的觸發(fā)條件

對于 Minor GC,其觸發(fā)條件非常簡單,當(dāng) Eden 空間滿時,就將觸發(fā)一次 Minor GC。而 Full GC 則相對復(fù)雜,有以下條件:

1. 調(diào)用 System.gc()

只是建議虛擬機執(zhí)行 Full GC,但是虛擬機不一定真正去執(zhí)行。不建議使用這種方式,而是讓虛擬機管理內(nèi)存。

2. 老年代空間不足

老年代空間不足的常見場景為前文所講的大對象直接進入老年代、長期存活的對象進入老年代等。

為了避免以上原因引起的 Full GC,應(yīng)當(dāng)盡量不要創(chuàng)建過大的對象以及數(shù)組。除此之外,可以通過 -Xmn 虛擬機參數(shù)調(diào)大新生代的大小,讓對象盡量在新生代被回收掉,不進入老年代。還可以通過 -XX:MaxTenuringThreshold 調(diào)大對象進入老年代的年齡,讓對象在新生代多存活一段時間。

3. 空間分配擔(dān)保失敗

使用復(fù)制算法的 Minor GC 需要老年代的內(nèi)存空間作擔(dān)保,如果擔(dān)保失敗會執(zhí)行一次 Full GC。具體內(nèi)容請參考上面的第 5 點。

4. JDK 1.7 及以前的永久代空間不足

在 JDK 1.7 及以前,HotSpot 虛擬機中的方法區(qū)是用永久代實現(xiàn)的,永久代中存放的為一些 Class 的信息、常量、靜態(tài)變量等數(shù)據(jù)。

當(dāng)系統(tǒng)中要加載的類、反射的類和調(diào)用的方法較多時,永久代可能會被占滿,在未配置為采用 CMS GC 的情況下也會執(zhí)行 Full GC。如果經(jīng)過 Full GC 仍然回收不了,那么虛擬機會拋出 java.lang.OutOfMemoryError。

為避免以上原因引起的 Full GC,可采用的方法為增大永久代空間或轉(zhuǎn)為使用 CMS GC。

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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