JVM13-垃圾回收算法


1. 標記階段算法

簡單來講,垃圾回收 分紅兩步, 第一步找出垃圾,第二步進行回收,而標記階段使用的算法,就是 爲了找出誰是垃圾html

  1. 在堆裏存放着幾乎全部的Java對象實例,在GC執行垃圾回收以前,首先須要區分出內存中哪些是存活對象,哪些是已經死亡的對象。
  2. 只有被標記爲己經死亡的對象,GC纔會在執行垃圾回收時,釋放掉其所佔用的內存空間,所以這個過程咱們能夠稱爲垃圾標記階段。
  3. 那麼在JVM中到底是如何標記一個死亡對象呢?簡單來講,當一個對象已經再也不被任何的存活對象繼續引用時,就能夠宣判爲已經死亡。
  4. 判斷對象存活通常有兩種方式:引用計數算法和可達性分析算法。

1.1 引用計數算法

  1. 引用計數算法(Reference Counting)比較簡單,對每一個對象保存一個整型的引用計數器屬性。用於記錄對象被引用的狀況。
  2. 對於一個對象A,只要有任何一個對象引用了A,則A的引用計數器就加1;當引用失效時,引用計數器就減1。只要對象A的引用計數器的值爲0,即表示對象A不可能再被使用,可進行回收。

優勢:java

  • 實現簡單,垃圾對象便於辨識;
  • 斷定效率高,回收沒有延遲性。

缺點:算法

  1. 它須要單獨的字段存儲計數器,這樣的作法增長了存儲空間的開銷。
  2. 每次賦值都須要更新計數器,伴隨着加法和減法操做,這增長了時間開銷。
  3. 引用計數器有一個嚴重的問題,即沒法處理循環引用的狀況。這是一條致命缺陷,致使在Java的

由於第三點的嚴重性,JAVA 垃圾回收器中沒有使用這類算法。數據庫

什麼是 循環引用緩存

1606039051270

上面的圖中 , 對象一引用了對象二 , 對象二引用了對象三, 而對象三又從新指向了對象一,併發

而對象一是被外部引用的,因此它的計數器是2,eclipse

可是當外部的引用斷掉時, 計數器減1,仍然是1, 不會被清除,致使這三個對象 沒法清除,形成內存泄漏jvm

使用代碼證實JAVA 中沒有使用引用計數算法ide

public class RefCountGC {
    //這個成員屬性惟一的做用就是佔用一點內存
    private byte[] bigSize = new byte[5 * 1024 * 1024];//5MB

    Object reference = null;

    public static void main(String[] args) {
        RefCountGC obj1 = new RefCountGC();
        RefCountGC obj2 = new RefCountGC();

        obj1.reference = obj2;
        obj2.reference = obj1;

        obj1 = null;
        obj2 = null;

        //顯式的執行垃圾回收行爲,這裏發生GC,obj1和obj2可否被回收?
        // System.gc();
    }
}複製代碼

上面代碼的內存示意圖:函數

1606039876939

下面運行驗證一下

jvm參數: -XX:+PrintGCDetails 打印GC日誌

不進行垃圾回收時:使用了 16798k

1606039759529

手動進行GC: 只剩下了655k , 說明 這兩個對象確實被回收了

1606039825511

引用計數小結

  1. 引用計數算法,是不少語言的資源回收選擇,例如因人工智能而更加火熱的Python,它是同時支持引用計數和垃圾收集機制。
  2. 具體哪一種最優是要看場景的,業界有大規模實踐中僅保留引用計數機制,以提升吞吐量的嘗試。
  3. Java並無選擇引用計數,是由於其存在一個基本的難題,也就是很難處理循環引用關係。Python如何解決循環引用?
  • 手動解除:很好理解,就是在合適的時機,代碼中手動解除引用關係。
  • 使用弱引用weakref,weakref是Python提供的標準庫,旨在解決循環引用。

1.2 可達性分析算法

可達性分析算法:也能夠稱爲根搜索算法、追蹤性垃圾收集

  1. 相對於引用計數算法而言,可達性分析算法不只一樣具有實現簡單和執行高效等特色,更重要的是該算法能夠有效地解決在引用計數算法中循環引用的問題,防止內存泄漏的發生。
  2. 相較於引用計數算法,這裏的可達性分析就是Java、C#選擇的。這種類型的垃圾收集一般也叫做追蹤性垃圾收集(Tracing Garbage Collection)

基本思路以下:

  1. 可達性分析算法是以根對象集合(GCRoots)爲起始點,按照從上至下的方式搜索被根對象集合所鏈接的目標對象是否可達。
  2. 使用可達性分析算法後,內存中的存活對象都會被根對象集合直接或間接鏈接着,搜索所走過的路徑稱爲引用鏈(Reference Chain)
  3. 若是目標對象沒有任何引用鏈相連,則是不可達的,就意味着該對象己經死亡,能夠標記爲垃圾對象。
  4. 在可達性分析算法中,只有可以被根對象集合直接或者間接鏈接的對象纔是存活對象。

示意圖:

1606045953206

GC Roots 能夠是哪些元素

列舉:

  1. 虛擬機棧中引用的對象,好比:各個線程被調用的方法中使用到的參數、局部變量等。
  2. 本地方法棧內JNI(一般說的本地方法)引用的對象方法區中類靜態屬性引用的對象,好比:Java類的引用類型靜態變量
  3. 方法區中常量引用的對象,好比:字符串常量池(StringTable)裏的引用
  4. 全部被同步鎖synchronized持有的對象
  5. Java虛擬機內部的引用。
  6. 基本數據類型對應的Class對象,一些常駐的異常對象(如:NullPointerException、OutofMemoryError),系統類加載器。
  7. 反映java虛擬機內部狀況的JMXBean、JVMTI中註冊的回調、本地代碼緩存等。

總結一句話就是,除了堆空間外的一些結構,好比:虛擬機棧、本地方法棧、方法區、字符串常量池等地方對堆空間進行引用的,均可以做爲GC Roots進行可達性分析

擴展:

除了這些固定的GC Roots集合之外,根據用戶所選用的垃圾收集器以及當前回收的內存區域不一樣,還能夠有其餘對象「臨時性」地加入,共同構成完整GC Roots集合。好比:在進行分代收集和局部回收時(PartialGC)。

若是隻針對Java堆中的某一塊區域進行垃圾回收(好比:典型的只針對新生代),必須考慮到這個區域的對象徹底有可能被其餘堆區域的對象所引用,例如老年代等,這時候就須要一併將關聯的區域對象也加入GC Roots集合中去考慮,才能保證可達性分析的準確性。

總結:

  • 在進行可達性分析時, 要對目標區域進行隔離, 通常將目標區域外的對象做爲 GC Roots,
  • 例如在大多數GC收集整個堆空間時, 就將堆外的對象,例如方法區做爲GC Roots 的對象
  • 若是在特殊的 GC 中,單獨收集新生代,,就須要將新生代除外的區域的對象都考慮到,好比 老年代中引用新生代對象,此時老年代的對象也能夠做爲 GC Roots

可達性分析注意事項

  1. 若是要使用可達性分析算法來判斷內存是否可回收,那麼分析工做必須在一個能保障一致性的快照中進行。這點不知足的話分析結果的準確性就沒法保證。
  2. 這點也是致使GC進行時必須「Stop The World」的一個重要緣由。即便是號稱(幾乎)不會發生停頓的CMS收集器中,枚舉根節點時也是必需要停頓的。

回到頂部

2. 對象的 finalization 機制

  1. Java語言提供了對象終止(finalization)機制來容許開發人員提供對象被銷燬以前的自定義處理邏輯。
  2. 當垃圾回收器發現沒有引用指向一個對象,即:垃圾回收此對象以前,標記階段,總會先調用這個對象的finalize()方法。
  3. finalize() 方法容許在子類中被重寫,用於在對象被回收時進行資源釋放。一般在這個方法中進行一些資源釋放和清理的工做,好比關閉文件、套接字和數據庫鏈接等。

注意 finaliza() 方法並非一定是銷燬前調用的, 它也是肯定此對象可不能夠被銷燬的一個判斷因素,在標記階段調用

Object 類中 finalize() 源碼

// 等待被重寫
protected void finalize() throws Throwable { }複製代碼

2.1 finalize() 方法使用的注意事項

  1. 永遠不要主動調用某個對象的finalize()方法應該交給垃圾回收機制調用。

  • finalize() 方法是能夠在標記階段致使對象復活的,可是若是手動執行過,那麼將不在判斷(finalize方法只可調用一次)
  • finalize()方法的執行時間是沒有保障的,它徹底由GC線程決定,極端狀況下,若不發生GC,則finalize()方法將沒有執行機會。由於jvm中有專門執行對象finalize方法的線程,此線程優先級比較低,即便主動調用該方法,也不會所以就直接進行回收

一個糟糕的finalize()會嚴重影響GC的性能(寫個多重循環, 每一個對象在標記時調用時,均可能執行)。

從功能上來講,finalize()方法與C++中的析構函數比較類似,可是Java採用的是基於垃圾回收器的自動內存管理機制,因此finalize()方法在本質上不一樣於C++中的析構函數。

2.2 對象的三種可能的狀態

因爲finalize()方法的存在,可能會將對象復活,因此虛擬機中的對象通常處於三種可能的狀態。

若是從全部的根節點都沒法訪問到某個對象,說明對象己經再也不使用了。通常來講,此對象須要被回收。

但事實上,也並不是是「非死不可」的,這時候它們暫時處於「緩刑」階段(關入大牢)。一個沒法觸及的對象有可能在某一個條件下「復活」本身,若是這樣,那麼對它當即進行回收就是不合理的

爲此,定義虛擬機中的對象可能的三種狀態。以下:

  1. 可觸及的:從根節點開始,能夠到達這個對象。
  2. 可復活的:對象的全部引用都被釋放,可是對象有可能在finalize()中復活。(全部引用所有釋放,第一次標記)
  3. 不可觸及的:對象的finalize()被調用,並無從新使GC Roots跟節點的對象引用本身(關入大牢的對象沒有找到關係),因此沒有復活,那麼就會進入不可觸及狀態。不可觸及的對象不可能被複活,由於finalize()只會被調用一次。(finalize方法沒有復活本身,第二次標記)

以上3種狀態中,是因爲finalize()方法的存在,進行的區分。只有在對象兩次標記,不可觸及時才能夠被回收。

2.3 finalize() 具體執行過程

上一節已經說到, 斷定一個對象objA是否可回收,至少要經歷兩次標記過程:

  1. 若是對象objA到GC Roots沒有引用鏈,則進行第一次標記。
  2. 進行篩選,判斷此對象是否有必要執行finalize()方法
  • 若是對象objA沒有重寫finalize()方法,或者finalize()方法已經被虛擬機調用過,則虛擬機視爲「沒有必要執行」,objA直接 斷定爲不可觸及的。(沒有關係,或者關係已經找過了)
  • 若是對象objA重寫了finalize()方法,且還未執行過,那麼objA會被插入到F-Queue隊列中,由一個虛擬機自動建立的、低優先級的Finalizer線程觸發其finalize()方法執行。(若是關入大牢的有可能有關係,並且沒有找過,則把他們都放到一個房間中,讓他們打電話)
finalize()方法是對象逃脫死亡的最後機會,稍後GC會對F-Queue隊列中的對象進行第二次標記。若是objA在finalize()方法中與引用鏈上的任何一個對象創建了聯繫,那麼在第二次標記時,objA會被移出「即將回收」集合。(若是找到打電話攀上關係,則復活)以後,對象若再次出現沒有引用存在的狀況。在這個狀況下,finalize()方法不會被再次調用,對象會直接變成不可觸及的狀態,也就是說,一個對象的finalize()方法只會被調用一次。(若是復活後,再次關進來,則不會再讓他找關係,直接標記第二次)

使用代碼證實上述觀點

下面的代碼中, 使用類變量做爲 GC Roots ,而且在對象回收時,在finalize 方法裏自救

public class CanReliveObj {
    public static CanReliveObj obj;//類變量,屬於 GC Root


    //此方法只能被調用一次
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("調用當前類重寫的finalize()方法");
        obj = this;//當前待回收的對象在finalize()方法中與引用鏈上的一個對象obj創建了聯繫
    }


    public static void main(String[] args) {
        try {
            obj = new CanReliveObj();
            // 對象第一次成功拯救本身
            obj = null;
            System.gc();//調用垃圾回收器
            System.out.println("第1次 gc");
            // 由於Finalizer線程優先級很低,暫停2秒,以等待它
            Thread.sleep(2000);
            if (obj == null) {
                System.out.println("obj is dead");
            } else {
                System.out.println("obj is still alive");
            }
            System.out.println("第2次 gc");
            // 下面這段代碼與上面的徹底相同,可是此次自救卻失敗了
            obj = null;
            System.gc();
            // 由於Finalizer線程優先級很低,暫停2秒,以等待它
            Thread.sleep(2000);
            if (obj == null) {
                System.out.println("obj is dead");
            } else {
                System.out.println("obj is still alive");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}複製代碼
  • 將finalize 方法註釋的狀況

打印:

第1次 gc
obj is dead
第2次 gc
obj is dead複製代碼

說明此對象在第一次 gc時直接就回收了

  • 將註釋放開

打印:

調用當前類重寫的finalize()方法
第1次 gc
obj is still alive
第2次 gc
obj is dead複製代碼

在第一次gc時 ,調用了 finalize 方法,並又從新使類變量指向本身,復活

可是在第二次gc 時,發現finalize 方法 就沒有再執行了,直接被回收

回到頂部

3. GC Roots 溯源

本節將介紹使用各個工具查看 GC Roots 集合

3.1 MAT 工具查看

  1. MAT是Memory Analyzer的簡稱,它是一款功能強大的Java堆內存分析器。用於查找內存泄漏以及查看內存消耗狀況。
  2. MAT是基於Eclipse開發的,是一款免費的性能分析工具。
  3. 你們能夠在http://www.eclipse.org/mat/下載並使用MAT

獲取 dump 文件

jvm的一個內存快照,能夠被各個軟件分析,

下面將 演示如何將正在運行的 程序導出 dump文件

public class GCRootsTest {
    public static void main(String[] args) {
        List<Object> numList = new ArrayList<>();
        Date birth = new Date();

        for (int i = 0; i < 100; i++) {
            numList.add(String.valueOf(i));
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("數據添加完畢,請操做:");
        new Scanner(System.in).next();
        numList = null;
        birth = null;

        System.out.println("numList、birth已置空,請操做:");
        new Scanner(System.in).next();

        System.out.println("結束");
    }
}複製代碼

先將程序跑起來,將阻塞

方式一:命令行使用jmap

1606136218303

方式二:使用JVisualVM

第一步: 選中監視tab, 點擊堆 Dump

1606136419481

第二步: 右擊另存爲

1606136477149

第三步: 將上面程序 鍵盤輸入,繼續執行, 捕獲第二個快照

1606136547696

這樣咱們就獲取到了兩個 內存快照, 一個是被局部變量引用的,一個是釋放掉的

如何使用MAT 查看堆內存快照

打開 MAT ,選擇 File --> Open Heap Dump, 選擇 須要查看的Dump文件

1606136708108

選擇 Java Basics --> GC Roots

1606137000897

前後查看兩個快照, 因爲 局部變量再也不引用對象, 因此不在是GC Roots

1606136886631

釋 放後:

1606136908431

3.2 JProfiler 工具使用

不用 dump文件, 查看實時的 運行時程序

查看當前程序中堆中最多的對象類型,並查看其GC Roots

點擊 :Live Memory --> All Object ,查看 堆中最多的對象, 並右擊 ,點擊Show Selection In Heap Walker

1606137684003

在顯示界面 選擇 References tab,查看堆中該類型的全部實例, 而後能夠選中某一個對象,選擇 Incoming References 選項, 再點擊 Show Paths To FC Roots 按鈕,彈出框點擊確認

1606137956342

而後就能夠看到 選中的對象的GC Roots , 例以下面的案例中, 字符串 "添加完畢,請操做" 對象 的 GC Roots 就是 out 對象, 由於被 System.out.println("添加完畢,請操做")打印

1606138263123

使用JProfiler 分析OOM

代碼:

public class HeapOOM {
    byte[] buffer = new byte[1 * 1024 * 1024];//1MB

    public static void main(String[] args) {
        ArrayList<HeapOOM> list = new ArrayList<>();

        int count = 0;
        try{
            while(true){
                list.add(new HeapOOM());
                count++;
            }
        }catch (Throwable e){
            System.out.println("count = " + count);
            e.printStackTrace();
        }
    }
}複製代碼

上面的代碼 將會致使OOM, 能夠開啓jvm指令,在出現OOM 時 自動生成 dump文件

運行程序,jvm指令: -Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

輸出日誌: 出現了OOM,生成的dump文件在 工程目錄下

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid14608.hprof ...
java.lang.OutOfMemoryError: Java heap space
	at com.atguigu.java.HeapOOM.<init>(HeapOOM.java:12)
	at com.atguigu.java.HeapOOM.main(HeapOOM.java:20)
Heap dump file created [7797849 bytes in 0.010 secs]
count = 6複製代碼

打開JProfiler, 能夠在 超大對象 裏面找到它

1606138829393

也能夠 查看出現OOM的線程:

1606138873872

回到頂部

4. 垃圾清除階段

上面第一節中, 說到了如何標記垃圾,那麼下面就開始清除垃圾,關於清除垃圾,也有不一樣的算法

目前在JVM中比較常見的三種垃圾收集算法是

  1. 標記-清除算法(Mark-Sweep)
  2. 複製算法(Copying)
  3. 標記-壓縮算法(Mark-Compact)

4.1 標記——清除 算法

標記-清除算法(Mark-Sweep)是一種很是基礎和常見的垃圾收集算法,該算法被J.McCarthy等人在1960年提出並並應用於Lisp語言。

執行過程

當堆中的有效內存空間(available memory)被耗盡的時候,就會中止整個程序(也被稱爲stop the world),而後進行兩項工做,第一項則是標記,第二項則是清除

**標記:**垃圾收集器從引用根節點開始遍歷,標記全部被引用的對象。(這裏是標記不是垃圾的對象)

  • 通常是在對象的Header中記錄爲可達對象。
  • 注意:標記的是引用的對象,不是垃圾!!

**清除:**垃圾收集器對堆內存從頭至尾進行線性的遍歷,若是發現某個對象在其Header中沒有標記爲可達對象,則將其回收

流程示意圖, 先從跟節點 找出全部的 可達對象, 標記爲"綠色",再遍歷整個對象列表,將沒有標記爲綠色的清除

1606140274876

何爲清除?

這裏所謂的清除並非真的置空,而是把須要清除的對象地址回收,保存在空閒的地址列表裏。下次有新對象須要加載時,判斷垃圾的位置空間是否夠,若是夠,就覆蓋原有的地址。 (跟電腦硬盤的刪除同樣)

關於空閒列表是在爲對象分配內存的時候提過:

若是內存規整

  • 採用指針碰撞的方式進行內存分配

若是內存不規整

  • 虛擬機須要維護一個空閒列表
  • 採用空閒列表分配內存

標記-清除算法的缺點

標記清除算法的優勢很明顯, 簡單 易理解 易於實現,可是缺點也很明顯

  1. 標記清除算法的效率不算高
  2. 在進行GC的時候,須要中止整個應用程序,用戶體驗較差
  3. 這種方式清理出來的空閒內存是不連續的,產生內存碎片,須要維護一個空閒列表

4.2 複製算法

  1. 爲了解決標記-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky於1963年發表了著名的論文,「使用雙存儲區的Lisp語言垃圾收集器CA LISP Garbage Collector Algorithm Using Serial Secondary Storage)」。
  2. M.L.Minsky在該論文中描述的算法被人們稱爲複製(Copying)算法,它也被M.L.Minsky本人成功地引入到了Lisp語言的一個實現版本中。

核心思路:

將存放對象的內存空間分爲兩塊,每次只使用其中一塊,在垃圾回收時,垃圾回收器也從跟節點開始遍歷,找到全部的可達對象,可是此時不標記, 而是直接將此對象複製到未被使用的內存塊中,以後全盤清除正在使用的內存塊中的全部對象,交換兩個內存的角色,最後完成垃圾回收

示意圖:

1606141538254

把可達的對象,直接複製到另一個區域中複製完成後,from區裏面的對象就沒有用了,新生代裏面就用到了複製算法

1606141576843

複製算法的優缺點

優勢

  1. 沒有標記和清除過程,實現簡單,運行高效
  2. 複製過去之後保證空間的連續性,不會出現「碎片」問題。

缺點

  1. 此算法的缺點也是很明顯的,就是須要兩倍的內存空間。
  2. 由於對象的地址發生了改變,全部對此對象的使用的地方的引用都須要改變

注意事項

若是系統中的垃圾對象不少,複製算法須要複製的存活對象數量並不會太大,效率較高,可是若是垃圾對象很是少的狀況, 每次拷貝都幾乎所有拷貝了,而後清除也就清除了個寂寞,

因此在jvm 中新生代中, 因爲垃圾回收頻率高,數量多,一次一般能夠回收70% - 99% 的內存空間 ,回收性價比很高。因此如今的商業虛擬機都是用這種收集算法回收新生代。

4.3 標記 - 壓縮(或標記-整理,Mark - Compact) 算法

複製算法的高效性是創建在存活對象少、垃圾對象多的前提下的。這種狀況在新生代常常發生,可是在老年代,更常見的狀況是大部分對象都是存活對象。

若是依然使用複製算法,因爲存活對象較多,複製的成本也將很高。所以,基於老年代垃圾回收的特性,須要使用其餘的算法。

標記-清除算法的確能夠應用在老年代中,可是該算法不只執行效率低下,並且在執行完內存回收後還會產生內存碎片,因此JVM的設計者須要在此基礎之上進行改進。標記-壓縮(Mark-Compact)算法由此誕生。

1970年先後,G.L.Steele、C.J.Chene和D.s.Wise等研究者發佈標記-壓縮算法。在許多現代的垃圾收集器中,人們都使用了標記-壓縮算法或其改進版本。

執行流程:

  1. 第一階段和標記清除算法同樣,從根節點開始標記全部被引用對象
  2. 第二階段將全部的存活對象壓(或者說是整理)到內存的一端,按順序排放。以後,清理邊界外全部的空間。

示意圖:

1606142659064

標記-壓縮算法與標記-清除算法的比較

標記-壓縮算法的最終效果等同於標記-清除算法執行完成後,再進行一次內存碎片整理,所以,也能夠把它稱爲標記-清除-壓縮(Mark-Sweep-Compact)算法。

兩者的本質差別在於

  • 標記-清除算法是一種非移動式的回收算法,
  • 標記-壓縮是移動式的。

並且在對象分配內存時能夠看到,若內存區域是零散的,須要訪問空閒列表(標記-清除算法回收地址到空閒列表)

可是若是使用標記-壓縮算法, 標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉。如此一來,當咱們須要給新對象分配內存時,JVM只須要持有一個內存的起始地址便可,這比維護一個空閒列表顯然少了許多開銷。

是否移動回收後的存活對象是一項優缺點並存的風險決策。

標記-壓縮算法的優缺點

優勢

  1. 消除了標記-清除算法當中,內存區域分散的缺點,咱們須要給新對象分配內存時,JVM只須要持有一個內存的起始地址便可。
  2. 消除了複製算法當中,內存減半的高額代價。

缺點

  1. 從效率上來講,標記-整理算法要低於複製算法和標記清除-算法
  2. 移動對象的同時,若是對象被其餘對象引用,則還須要調整引用的地址
  3. 移動過程當中,須要全程暫停用戶應用程序。即:STW

4.4 三種清除算法的VS

三種算法的橫縱對比:

標記清除

標記整理

複製

速率

中等

最慢

最快

空間開銷

少(但會堆積碎片)

少(不堆積碎片)

一般須要活對象的2倍空間(不堆積碎片)

移動對象

  1. 效率上來講,複製算法是當之無愧的老大,可是卻浪費了太多內存。
  2. 而爲了儘可能兼顧上面提到的三個指標,標記-整理算法相對來講更平滑一些,可是效率上不盡如人意,它比複製算法多了一個標記的階段,比標記-清除多了一個整理內存的階段。

總結: 沒有最好的算法,只有最適合的算法

4.5 分代收集算法

前面全部這些算法中,並無一種算法能夠徹底替代其餘算法,它們都具備本身獨特的優點和特色。

分代收集算法應運而生。他的目標不是替換上面的算法,而是具體問題 具體對待

分代收集算法,是基於這樣一個事實:不一樣的對象的生命週期是不同的。所以,不一樣生命週期的對象能夠採起不一樣的收集方式,以便提升回收效率。

通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色使用不一樣的回收算法,以提升垃圾回收的效率。

在Java程序運行的過程當中,會產生大量的對象,其中有些對象是與業務信息相關:

  • 好比Http請求中的Session對象、線程、Socket鏈接,這類對象跟業務直接掛鉤,所以生命週期比較長。
  • 可是還有一些對象,主要是程序運行過程當中生成的臨時變量,這些對象生命週期會比較短,好比:String對象,因爲其不變類的特性,系統會產生大量的這些對象,有些對象甚至只用一次便可回收。

目前幾乎全部的GC都採用分代收集算法執行垃圾回收的

每一個代的各個特色和適合的回收算法

年輕代(Young Gen)

  • 年輕代特色:區域相對老年代較小,對象生命週期短、存活率低,回收頻繁。
  • 這種狀況複製算法的回收整理,速度是最快的。複製算法的效率只和當前存活對象大小有關,所以很適用於年輕代的回收。而複製算法內存利用率不高的問題,經過hotspot中的兩個survivor的設計獲得緩解。(整體佔用比例較小)

老年代(Tenured Gen)

  • 老年代特色:區域較大,對象生命週期長、存活率高,回收不及年輕代頻繁。
  • 這種狀況存在大量存活率高的對象,複製算法明顯變得不合適。通常是由標記-清除或者是標記-清除與標記-整理的混合實現。
    • Mark(標記)階段的開銷與存活對象的數量成正比。標記的是存活的對象,存活的越多,標記的越多,越慢
    • Sweep階段(標記清除的清除階段)的開銷與所管理區域的大小成正相關。全盤遍歷全部須要清理的區域, 管理的區域越大, 越慢
    • Compact階段(標記整理的整理階段)的開銷與存活對象的數據成正比。存活的越多,整理的對象越多,越慢

簡單介紹CMS 回收器

  1. 以HotSpot中的CMS回收器爲例,CMS是基於Mark-Sweep實現的,對於對象的回收效率很高。
  2. 對於碎片問題,CMS採用基於Mark-Compact算法的Serial Old回收器做爲補償措施:當內存回收不佳(碎片致使的Concurrent Mode Failure時),將採用Serial Old執行Full GC以達到對老年代內存的整理。
  3. 分代的思想被現有的虛擬機普遍使用。幾乎全部的垃圾回收器都區分新生代和老年代

4.6 增量收集算法

上述現有的算法,在垃圾回收過程當中,應用軟件將處於一種Stop the World的狀態。在Stop the World狀態下,應用程序全部的線程都會掛起,暫停一切正常的工做,等待垃圾回收的完成。

若是垃圾回收時間過長,應用程序會被掛起好久,將嚴重影響用戶體驗或者系統的穩定性。爲了解決這個問題,即對實時垃圾收集算法的研究直接致使了增量收集(Incremental Collecting)算法的誕生。

基本思路:

  1. 若是一次性將全部的垃圾進行處理,須要形成系統長時間的停頓,那麼就可讓垃圾收集線程和應用程序線程交替執行。每次,垃圾收集線程只收集一小片區域的內存空間,接着切換到應用程序線程。依次反覆,直到垃圾收集完成。
  2. 總的來講,增量收集算法的基礎還是傳統的標記-清除和複製算法。增量收集算法經過對線程間衝突的妥善處理,容許垃圾收集線程以分階段的方式完成標記、清理或複製工做

使用這種方式,因爲在垃圾回收過程當中,間斷性地還執行了應用程序代碼,因此能減小系統的停頓時間。

缺點:

由於線程切換和上下文轉換的消耗,會使得垃圾回收的整體成本上升,形成系統吞吐量的降低。

4.7 分區算法

  1. 通常來講,在相同條件下,堆空間越大,一次GC時所須要的時間就越長,有關GC產生的停頓也越長。
  2. 爲了更好地控制GC產生的停頓時間,將一塊大的內存區域分割成多個小塊,根據目標的停頓時間,每次合理地回收若干個小區間,而不是整個堆空間,從而減小一次GC所產生的停頓。
  3. 分代算法將按照對象的生命週期長短劃分紅兩個部分,分區算法將整個堆空間劃分紅連續的不一樣小區間。每個小區間都獨立使用,獨立回收。這種算法的好處是能夠控制一次回收多少個小區間。

1606145774237


5. 寫在最後

注意,這些只是基本的算法思路,實際GC回收器過程要複雜的多,目前還在發展中的前沿GC都是複合算法,而且並行和併發兼備。 因此這裏以爲模糊的,到後面把各個GC 回收器的實現說明完,就清晰了.

相關文章
相關標籤/搜索