天天看點

面試官:你了解GC嗎?一文讀懂分代回收機制

本文思維導圖:

面試官:你了解GC嗎?一文讀懂分代回收機制

GC概述

gc全拼Garbage Collection,顧名思義垃圾回收的意思,它的主要作用就是回收程式中不再使用的記憶體。

那我們首先需要知道是否可以主動通知jvm進行垃圾回收?在Java中不能實時調用垃圾回收器對某個對象或者所有對象進行垃圾回收,但是可以通過System.gc()方法來通知垃圾回收器運作,當然,jvm也并不保證垃圾回收器馬上就會運作。由于System.gc()方法的執行會停止所有的響應,去檢查記憶體是否有可回收的對象,對程式的正常運作和性能造成了威脅,是以該方法不能頻繁使用而且使強烈不推薦使用。正所謂用人不疑疑人不用,那麼了解GC回收機制至關重要。下面會提出幾個問題作為本文的開篇。

Java 與 C++等語言最大的技術差別?自動化的垃圾回收機制(GC)

為什麼要了解 GC 和記憶體配置設定政策?1、面試需要 2、GC 對應用的性能是有影響的; 3、寫代碼有好處 

JVM中哪些地方會被回收?棧:棧中的生命周期是跟随線程,是以一般不需要關注

堆:堆中的對象是垃圾回收的重點

方法區/元空間:這一塊也會發生垃圾回收,不過這塊的效率比較低,一般不是我們關注的重點

問題來了雖然Java不需要開發人員手動管理記憶體回收,但是你有沒有想過,這些記憶體使如何被回收的?什麼是需要被回收的?回收的算法它們各有什麼優勢?下面我将重點講解這幾個問題

分代回收理論

垃圾回收主要發生在堆區,我們先留下垃圾回收的整體架構,在對每一個點進行剖析。

目前商業虛拟機的垃圾回收器,大多遵循“分代收集”的理論來進行設計,這個理論大體上是這麼描述的: 1、 絕大部分的對象都是朝生夕死。 2、 熬過多次垃圾回收的對象就越難回收。 根據以上兩個理論,朝生夕死的對象放一個區域,難回收的對象放另外一個區域,這個就構成了新生代(eden,from,to)和老年代(tenured)。

面試官:你了解GC嗎?一文讀懂分代回收機制

GC種類

市面上發生垃圾回收的叫法很多,我大體整理了一下:

1、 新生代回收(MinorGC / YoungGC):指隻是進行新生代的回收。

2、 老年代回收(MajorGC / OldGC):指隻是進行老年代的回收。目前隻有 CMS 垃圾回收器會有這個單獨的回收老年代的行為。 (MajorGC 定義是比較混亂,有說指是老年代,有的說是做整個堆的收集,這個需要你根據别人的場景來定,沒有固定的說法)

3、 整堆回收(FullGC):收集整個 Java 堆和方法區(注意包含方法區)

什麼是垃圾

在堆裡面存放着幾乎所有的對象執行個體,垃圾回收器在對對進行回收前,要做的事情就是确定這些對象中哪些還是“存活”着,哪些已經“死去”(死去 代表着不可能再被任何途徑使用得對象了) 什麼是垃圾?

C 語言申請記憶體:mallocfree C++: newdelete C/C++ 手動回收記憶體 Java:new Java 是自動記憶體回收,程式設計上簡單,系統不容易出錯。 手動釋放記憶體,容易出兩種類型的問題: 1、忘記回收 2、多次回收 沒有任何引用指向的一個對象或者多個對象(循環引用)

引用計數法

在對象中添加一個引用計數器,每當有一個地方引用它,計數器就加 1,當引用失效時,計數器減 1.

Python 在用,但主流虛拟機沒有使用,因為存在對象互相引用的情況,這個時候需要引入額外的機制來處理,這樣做影響效率。

/**
 * VM Args: -XX: +PrintGC
 * 判斷對象存活
 */
public class Isalive {
    public object instance =null;
    //占據記憶體,便于判斷分析GC
    private byte[] bigSize = new byte [10*1024*1024];
    public static void main(String[] args) {
        Isalive objectA = new Isalive();
        Isalive objectB = new Isalive();
        //互相引用
        objectA.instance = objectB;
        objectB.instance = objectA;
        //切斷可達
        objectA =null;
        objectB =null;
        //強制垃圾回收
        System.gc();
    }
}      
面試官:你了解GC嗎?一文讀懂分代回收機制

在代碼中看到,隻保留互相引用的對象還是被回收掉了,說明 JVM 中采用的不是引用計數法。

根可達性分析算法

來判定對象是否存活的。這個算法的基本思路就是通過一系列的稱為“GCRoots”的對象作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為 引用鍊(ReferenceChain),當一個對象到 GCRoots 沒有任何引用鍊相連時,則證明此對象是不可用的。

作為 GCRoots 的對象包括下面幾種(重點是前面 4 種):

一、虛拟機棧(棧幀中的本地變量表)中引用的對象;各個線程調用方法堆棧中使用到的參數、局部變量、臨時變量等。

二、方法區中類靜态屬性引用的對象;java 類的引用類型靜态變量。

三、方法區中常量引用的對象;比如:字元串常量池裡的引用。

四、本地方法棧中 JNI(即一般說的 Native 方法)引用的對象。

五、JVM 的内部引用(class 對象、異常對象 NullPointException、OutofMemoryError,系統類加載器)。(非重點)

六、所有被同步鎖(synchronized 關鍵)持有的對象。(非重點)  JVM 内部的 JMXBean、JVMTI 中注冊的回調、本地代碼緩存等(非重點)

七、JVM 實作中的“臨時性”對象,跨代引用的對象(在使用分代模型回收隻回收部分代的對象,這個後續會細講,先大緻了解概念)(非重點)

以上的回收都是對象,類的回收條件: 注意 Class 要被回收,條件比較苛刻,必須同時滿足以下的條件(僅僅是可以,不代表必然,因為還有一些參數可以進行控制):

1、該類所有的執行個體都已經被回收,也就是堆中不存在該類的任何執行個體。

2、加載該類的 ClassLoader 已經被回收。

3、該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射通路該類的方法。

4、參數控制:廢棄的常量和靜态變量的回收其實就和 Class 回收的條件差不多。

Finalize 方法

即使通過可達性分析判斷不可達的對象,也不是“非死不可”,它還會處于“緩刑”階段,真正要宣告一個對象死亡,需要經過兩次标記過程,一次是 沒有找到與 GCRoots 的引用鍊,它将被第一次标記。随後進行一次篩選(如果對象覆寫了 finalize),我們可以在 finalize 中去拯救。

代碼示範:

/**
 * @author Mac
 * @date 2020/8/15 - 11:36
 */
public class FinalizeGC {
    public static FinalizeGC instance = null;

    public void isAlive() {
        System.out.println("I am still alivel");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed");
        FinalizeGC.instance = this;
    }

    public static void main(String[] args) throws Throwable {
        instance = new FinalizeGC();
        // 對象進行第1次GC
        instance = null;
        System.gc();
        Thread.sleep(1000); // Finolizer 方法優先級很低。需要等待
        if (instance != null) {
            instance.isAlive();
        } else {
            System.out.println("I am dead! ");
        }
        // 對象進行第2 XGC
        instance = null;
        System.gc();
        Thread.sleep(1000);
        if (instance != null) {
            instance.isAlive();
        } else {
            System.out.println("I am dead! ");
        }
    }
}      

運作結果: 

finalize method executed

I am still alivel

I am dead!  

可以看到,對象可以被拯救一次(finalize 執行第一次,但是不會執行第二次) 代碼改一下,再來一次。

代碼示範:

/**
 * @author YXH
 * @date 2020/8/15 - 11:36
 */
public class FinalizeGC {
    public static FinalizeGC instance = null;
    public void isAlive() {
        System.out.println("I am still alivel");
    }
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed");
        FinalizeGC.instance = this;
    }
    public static void main(String[] args) throws Throwable {
        instance = new FinalizeGC();
        // 對象進行第1次GC
        instance = null;
        System.gc();
        // Thread.sleep(1000); // Finolizer 方法優先級很低。需要等待
        if (instance != null) {
            instance.isAlive();
        } else {
            System.out.println("I am dead! ");
        }
        // 對象進行第2 XGC
        instance = null;
        System.gc();
        // Thread.sleep(1000);
        if (instance != null) {
            instance.isAlive();
        } else {
            System.out.println("I am dead! ");
        }
    }
}      

 運作結果:

finalize method executed

I am dead! 

I am dead! 

這次對象沒有被拯救,這個就是 finalize 方法執行緩慢,還沒有完成拯救,垃圾回收器就已經回收掉了。 是以建議大家盡量不要使用 finalize,因為這個方法太不可靠。在生産中你很難控制方法的執行或者對象的調用順序,建議大家忘了 finalize 方法!因為在 finalize 方法能做的工作,java 中有更好的,比如 try-finally 或者其他方式可以做得更好

四種引用

強應用

一般的 Objectobj=newObject() ,就屬于強引用。在任何情況下,隻有有強引用關聯(與根可達)還在,垃圾回收器就永遠不會回收掉被引用的對象。

軟引用

一些有用但是并非必需,用軟引用關聯的對象,系統将要發生記憶體溢出(OuyOfMemory)之前,這些對象就會被回收(如果這次回收後還是沒有足夠的 空間,才會抛出記憶體溢出)。參見代碼: VM 參數 -Xms10m -Xmx10m-XX:+PrintGC.

例如,一個程式用來處理使用者提供的圖檔。如果将所有圖檔讀入記憶體,這樣雖然可以很快的打開圖檔,但記憶體空間使用巨大,一些使用較少的圖檔浪費 記憶體空間,需要手動從記憶體中移除。如果每次打開圖檔都從磁盤檔案中讀取到記憶體再顯示出來,雖然記憶體占用較少,但一些經常使用的圖檔每次打開都 要通路磁盤,代價巨大。這個時候就可以用軟引用建構緩存。

弱引用

一些有用(程度比軟引用更低)但是并非必需,用弱引用關聯的對象,隻能生存到下一次垃圾回收之前,GC 發生時,不管記憶體夠不夠,都會被回收。 

虛引用

幽靈引用,最弱(随時會被回收掉) 垃圾回收的時候收到一個通知,就是為了監控垃圾回收器是否正常工作。

代碼示範:

import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.LinkedList;
import java.util.List;

/**
 * @author Mac
 * @date 2020/8/15 - 11:36
 */
public class GC {
    public static void main(String[] args) {
        User u = new User(1, "Mac"); //new是強引用
        SoftReference<User> userSoft = new SoftReference(u);
        u = null;   // 幹掉強引用,確定這個執行個體隻有userSoft的軟引用
        System.out.println(userSoft.get());
        System.gc();    // 進行一次GC垃圾回收
        System.out.println("After gc");
        System.out.println(userSoft.get());
        // 往堆中填充資料,導緻OOM
        List<byte[]> list = new LinkedList();
        try {
            for (int i = 0; i < 100; i++) {
                System.out.println("**********+userSoft.get()");
                list.add(new byte[1024 * 1024 * 1]); //1M的對象
            }
        } catch (Throwable e) {
            // 抛出了00M異常時列印軟引用對象
            System.out.println("Except*********" + userSoft.get());
        }

        WeakReference<User> userWeak = new WeakReference(u);
        u = null;//幹掉強引用,確定這個執行個體隻有userWeak的弱引用
        System.out.println(userWeak.get());
        System.gc();//進行一次GC垃圾回收
        System.out.println("After gc");
        System.out.println(userWeak.get());
    }
}      

垃圾回收算法

複制算法(Copying)

将可用記憶體按容量劃分為大小相等的兩塊,每次隻使用其中的一塊。當這一塊的記憶體用完了,就将還存活着的對象複制到另外一塊上面,然後再把已使 用過的記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體配置設定時也就不用考慮記憶體碎片等複雜情況,隻要按順序配置設定記憶體即可, 實作簡單,運作高效。隻是這種算法的代價是将記憶體縮小為了原來的一半。

但要注意:記憶體移動是必須實打實的移動(複制),是以對應的引用(直接指針)需要調整。 複制回收算法适合于新生代,因為大部分對象朝生夕死,那麼複制過去的對象比較少,效率自然就高,另外一半的一次性清理是很快的。

面試官:你了解GC嗎?一文讀懂分代回收機制

Appel 式回收

一種更加優化的複制回收分代政策:具體做法是配置設定一塊較大的 Eden 區和兩塊較小的 Survivor 空間(你可以叫做 From 或者 To,也可以叫做 Survivor1 和 Survivor2) 專門研究表明,新生代中的對象 98%是“朝生夕死”的,是以并不需要按照 1:1 的比例來劃分記憶體空間,而是将記憶體分為一塊較大的 Eden 空間和兩塊較 小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor[1]。當回收時,将 Eden 和 Survivor 中還存活着的對象一次性地複制到另外一塊 Survivor 空間上, 最後清理掉 Eden 和剛才用過的 Survivor 空間。 HotSpot 虛拟機預設 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用記憶體空間為整個新生代容量的 90%(80%+10%),隻有 10%的記憶體會被 “浪費”。當然,98%的對象可回收隻是一般場景下的資料,我們沒有辦法保證每次回收都隻有不多于 10%的對象存活,當 Survivor 空間不夠用時,需要 依賴其他記憶體(這裡指老年代)進行配置設定擔保(HandlePromotion)

面試官:你了解GC嗎?一文讀懂分代回收機制

标記-清除算法(Mark-Sweep)

算法分為“标記”和“清除”兩個階段:首先掃描所有對象标記出需要回收的對象,在标記完成後掃描回收所有被标記的對象,是以需要掃描兩遍。 回收效率略低,如果大部分對象是朝生夕死,那麼回收效率降低,因為需要大量标記對象和回收對象,對比複制回收效率要低。 它的主要問題,标記清除之後會産生大量不連續的記憶體碎片,空間碎片太多可能會導緻以後在程式運作過程中需要配置設定較大對象時,無法找到足夠的連 續記憶體而不得不提前觸發另一次垃圾回收動作。 回收的時候如果需要回收的對象越多,需要做的标記和清除的工作越多,是以标記清除算法适用于老年代。

面試官:你了解GC嗎?一文讀懂分代回收機制

标記-整理算法(Mark-Compact)