JVM_10 垃圾回收1-概述+相關算法

完整JVM學習筆記請戳

1.概述

什麼是垃圾

Java = (C++)--html

什麼是垃圾( Garbage) 呢?
 ➢垃圾是指在運行程序中沒有任何指針指向的對象,這個對象就是須要被回收的垃圾。
 ➢外文: An object is considered garbage when it can no longer be reached from any pointer in the runningprogram.
若是不及時對內存中的垃圾進行清理,那麼,這些垃圾對象所佔的內存空 間會一直保留到應用程序結束,被保留的空間沒法被其餘對象使用。甚至可能致使內存溢出。
  • 垃圾收集,不是Java語言的伴生產物。早在1960年,第一門開始使用內存動態分配和垃圾收集技術的Lisp語言誕生。
    • 關於垃圾收集有三個經典問題:
      • ➢哪些內存須要回收?
      • ➢何時回收?
      • ➢如何回收?
  • 垃圾收集機制是Java的招牌能力,極大地提升了開發效率。現在,垃圾收 集幾乎成爲現代語言的標配,即便通過如此長時間的發展,Java的垃圾收集機制仍然在不斷的演進中,不一樣大小的設備、不一樣特徵的應用場景,對垃圾收集提出了新的挑戰,這固然也是面試的熱點。

大廠面試題

  1. 螞蟻金服
  • 你知道哪幾種垃圾回收器,各自的優缺點,重點講一下 cms和g1
    • 一面: JVM GC算法有哪些,目前的JDK版本採用什麼回收算法
    • 一面: ( G1回收器講下回收過程
  • GC是什麼?爲何要有GC?
    • 一面: GC的兩種斷定方法? CMS收集器與G1收集器的特色。
  1. 百度
  • 說一下GC算法,分代回收說下
  • 垃圾收集策略和算法
  1. 天貓
  • 一面: jvm GC原理,JVM怎麼回收內存
  • 一面: CMS特色,垃圾回收算法有哪些?各自的優缺點,他們共同的缺點是什麼?
  1. 滴滴
  • 一面: java的垃圾回收器都有哪些,說下g1的應用場景,平時你是如何搭配使用垃圾回收器的
  1. 京東:
  • 你知道哪幾種垃圾收集器,各自的優缺點,重點講下cms和G1,包括原理,流程,優缺點。垃圾回收算法的實現原理。
  1. 阿里:
  • 講一講垃圾回收算法。
  • 什麼狀況下觸發垃圾回收?
  • 如何選擇合適的垃圾收集算法?
  • JVM有哪三種垃圾回收器?
  1. 字節跳動:
  • 常見的垃圾回收器算法有哪些,各有什麼優劣?
  • system.gc ()和runtime.gc()會作什麼事情?
  • 一面: Java GC機制? GC Roots有哪些?
  • 二面: Java對象的回收方式,回收算法。
  • CMS和G1瞭解麼,CMS解決什麼問題,說一下回收的過程。
  • CMS回收停頓了幾回,爲何要停頓兩次。

爲何須要GC

  • 對於高級語言來講,一個基本認知是若是不進行垃圾回收,內存早晚都會被消耗完,由於不斷地分配內存空間而不進行回收,就好像不停地生產生活垃圾而歷來不打掃同樣。
  • 除了釋放沒用的對象,垃圾回收也能夠清除內存裏的記錄碎片。碎片整理將所佔用的堆內存移到堆的一端,以便JVM 將整理出的內存分配給新的對象。
  • 隨着應用程序所應付的業務愈來愈龐大、複雜,用戶愈來愈多,沒有GC就不能保證應用程序的正常進行。而常常形成STW的GC又跟不上實際的需求,因此纔會不斷地嘗試對GC進行優化。

早期垃圾回收

  • 在早期的C/C++時代,垃圾回收基本.上是手工進行的。開發人員可使用 new關鍵字進行內存申請,並使用delete關鍵字進行內存釋放。好比如下代碼:
MibBridge *pBridge = new cmBaseGroupBridge () ;
//若是註冊失敗,使用Delete釋放該對象所佔內存區域
if (pBridge->Register(kDestroy)!= NO_ERROR)
delete pBridge;
複製代碼
  • 這種方式能夠靈活控制內存釋放的時間,可是會給開發人員帶來頻繁申請和釋放內存的管理負擔。假若有一處內存區間因爲程序員編碼的問題忘記被回收,那麼就會產生內存泄漏,垃圾對象永遠沒法被清除,隨着系統運行時間的不斷增加,垃圾對象所耗內存可能持續上升,直到出現內存溢出並形成應用程序崩潰。
  • 在有了垃圾回收機制後,上述代碼塊極有可能變成這樣:
MibBridge *pBridge = new cmBaseGroupBridge();
pBridge 一> Register(kDestroy);
複製代碼
  • 如今,除了Java之外,C#、Python、 Ruby等語言都使用了自動垃圾回收的思想,也是將來發展趨勢。能夠說,這種自動化的內存分配和垃圾回收的方式己經成爲現代開發語言必備的標準。

Java垃圾回收機制

  • 自動內存管理,無需開發人員手動參與內存的分配與回收,這樣下降內存泄漏和內存溢出的風險
    • 沒有垃圾回收器,java也會和cpp同樣,各類懸垂指針,野指針,泄露問題讓你頭疼不已。
  • 自動內存管理機制,將程序員從繁重的內存管理中釋放出來,能夠更專心地專一於業務開發
  • oracle官 網關於垃圾回收的介紹
  • 對於Java開發人員而言,自動內存管理就像是一個黑匣子,若是過分依賴於 「自動」,那麼這將會是一場災難,最嚴重的就會弱化Java開發人員在程序出現內存溢出時定位問題和解決問題的能力。
  • 此時,了 解JVM的自動內存分配和內存回收原理就顯得很是重要,只有在真 正瞭解JVM是如何管理內存後,咱們纔可以在碰見OutOfMemoryError時, 快速地根據錯誤異常日誌定位問題和解決問題。
  • 當須要排查各類內存溢出、內存泄漏問題時,當垃圾收集成爲系統達到更高 併發量的瓶頸時,咱們就必須對這些「自動化」的技術實施必要的監控和調節。
  • 垃圾回收器能夠對年輕代回收,也能夠對老年代回收,甚至是全堆和方法區的回收。
    • 其中,Java堆是垃圾收集器的工做重點。
  • 從次數上講:
    • 頻繁收集Young區
    • 較少收集01d區
    • 基本不動Perm區
    • 2

2. 垃圾回收相關算法

垃圾標記階段:對象存活判斷

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

2.1 標記階段:法1_引用計數法 (java沒有采用)

  • 引用計數算法(Reference Counting)比較簡單,對每一個對象保存一個整型 的引用計數器屬性。用於記錄對象被引用的狀況。
  • 對於一個對象A,只要有任何一個對象引用了A,則A的引用計數器就加1;當引用失效時,引用計數器就減1。只要對象A的引用計數器的值爲0,即表示對象A不可能再被使用,可進行回收。
  • 優勢:實現簡單,垃圾對象便於辨識;斷定效率高,回收沒有延遲性。
  • 缺點:
    • ➢它須要單獨的字段存儲計數器,這樣的作法增長了存儲空間的開銷。
    • ➢每次賦值都須要更新計數器,伴隨着加法和減法操做,這增長了時間開銷。
    • ➢引用計數器有一個嚴重的問題,即沒法處理循環引用的狀況。這是一 條致命缺陷,致使==在Java的垃圾回收器中沒有使用這類算法==。 3

圖示分析證實java沒有采用引用計數法
4java

若是不下當心直接把0bj1 一reference和0bj2 一reference置null。 則在Java堆當中的兩塊內存依然保持着互相引用,沒法回收。git

/**
 * -XX:+PrintGCDetails
 * 證實:java使用的不是引用計數算法
 */
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();

        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製代碼

小結

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

2.2 標記階段:法2_可達性分析算法

也叫根搜索算法或追蹤性垃圾收集程序員

  • 相對於引用計數算法而言,可達性分析算法不只一樣具有實現簡單和執行高 效等特色,更重要的是該算法能夠有效地解決在引用計數算法中循環引用的問題,防止內存泄漏的發生。
  • 相較於引用計數算法,這裏的可達性分析就是Java、C#選擇的。這種類型的垃圾收集一般也叫做追蹤性垃圾收集(Tracing GarbageCollection)。
  • 所謂"GC Roots"根集合就是一組必須活躍的引用。
  • 基本思路:
    • ➢可達性分析算法是以根對象集合(GCRoots)爲起始點,按照從上至下的方式搜索被根對象集合所鏈接的目標對象是否可達。
    • ➢使用可達性分析算法後,內存中的存活對象都會被根對象集合直接或間接鏈接着,搜索所走過的路徑稱爲引用鏈(Reference Chain)
    • ➢若是目標對象沒有任何引用鏈相連,則是不可達的,就意味着該對象己經死亡,能夠標記爲垃圾對象。
    • ➢在可達性分析算法中,只有可以被根對象集合直接或者間接鏈接的對象纔是存活對象。
    • 5

GC Roots

在Java語言中,GC Roots包括如下幾類元素:github

  • 虛擬機棧中引用的對象
    • ➢好比:各個線程被調用的方法中使用到的參數、局部變量等。
  • 本地方法棧內JNI(一般說的本地方法)引用的對象
  • 方法區中類靜態屬性引用的對象
    • ➢好比:Java類的引用類型靜態變量
  • 方法區中常量引用的對象
    • ➢好比:字符串常量池(string Table) 裏的引用
  • 全部被同步鎖synchroni zed持有的對象
  • Java虛擬機內部的引用。
    • ➢基本數據類型對應的Class對象,一些常駐的異常對象(如: NullPointerException、OutOfMemoryError) ,系統類加載器。
  • 反映java虛擬機內部狀況的JMXBean、JVMTI中註冊的回調、本地代碼緩存等
  • 除了這些固定的GCRoots集合之外,根據用戶所選用的垃圾收集器以及當 前回收的內存區域不一樣,還能夠有其餘對象「臨時性」地加入,共同構成完整GC Roots集合。好比:分代收集和局部回收(Partial GC)。
    • ➢若是隻針對Java堆中的某一塊區域進行垃圾回收(好比:典型的只針 對新生代),必須考慮到內存區域是虛擬機本身的實現細節,更不是孤立封閉的,這個區域的對象徹底有可能被其餘區域的對象所引用,這時候就須要一.並將關聯的區域對象也加入GC Roots集合中去考慮,才能保證可達性分析的準確性。
  • 小技巧:因爲Root採用棧方式存放變量和指針,因此若是一個指針,它保存了堆內存裏面的對象,可是本身又不存放在堆內存裏面,那它就是一個Root
  • 6

注意面試

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

2.3 對象的finalization機制

  • Java語言提供了對象終止(finalization)機制來容許開發人員提供對象被銷燬以前的自定義處理邏輯。
  • 當垃圾回收器發現沒有引用指向一個對象,即:垃圾回收此對象以前,總會先調用這個對象的finalize()方法。
  • finalize()方法容許在子類中被重寫,用於在對象被回收時進行資源釋放。一般在這個方法中進行一些資源釋放和清理的工做,好比關閉文件、套接字和數據庫鏈接等。
  • 應該交給垃圾回收機制調用。理由包括下面三點:永遠不要主動調用某個對象的finalize ()方法
    • ➢在finalize() 時可能會致使對象復活。
    • ➢finalize()方法的執行時間是沒有保障的,它徹底由Gc線程決定,極端狀況下,若不發生GC,則finalize() 方法將沒有執行機會。
    • ➢一個糟糕的finalize ()會嚴重影響GC的性能。
  • 從功能上來講,finalize()方法與C++ 中的析構函數比較類似,可是Java採用的是基於垃圾回收器的自動內存管理機制,因此finalize()方法在本質,上不一樣於C++ 中的析構函數。

對象是否"死亡"算法

  • 因爲finalize ()方法的存在,==虛擬機中的對象通常處於三種可能的狀態==
  • 若是從全部的根節點都沒法訪問到某個對象,說明對象己經再也不使用了。通常來講,此對象須要被回收。但事實上,也並不是是「非死不可」的,這時候它們暫時處於「緩刑」階段。==一個沒法觸及的對象有可能在某一個條件下「復活」本身==,若是這樣,那麼對它的回收就是不合理的,爲此,定義虛擬機中的對象可能的三種狀態。以下:
    • ➢==可觸及的==:從根節點開始,能夠到達這個對象。
    • ➢==可復活的==:對象的全部引用都被釋放,可是對象有可能在finalize()中復活。
    • ➢==不可觸及的==:對象的finalize()被調用,而且沒有復活,那麼就會進入不可觸及狀態。不可觸及的對象不可能被複活,由於finalize() 只會被調用一一次。
  • 以上3種狀態中,是因爲finalize()方法的存在,進行的區分。只有在對象不可觸及時才能夠被回收。 斷定是否能夠回收具體過程 斷定一個對象objA是否可回收,至少要經歷兩次標記過程:
  1. 若是對象objA到GC Roots沒有引用鏈,則進行第一 次標記。
  2. 進行篩選,判斷此對象是否有必要執行finalize()方法
    1. ①若是對 象objA沒有重寫finalize()方法,或者finalize ()方法已經被虛擬機調用過,則虛擬機視爲「沒有必要執行」,objA被斷定爲不可觸及的。
    2. ②若是對象objA重寫了finalize()方法,且還未執行過,那麼objA會被插入到F一Queue隊列中,由一個虛擬機自動建立的、低優先級的Finalizer線程觸發其finalize()方法執行。
    3. ③finalize()方法是對象逃脫死亡的最後機會,稍後Gc會對F一Queue隊列中的對象進行第二次標記。若是objA在finalize()方法中與引用鏈上的任何一個對象創建了聯繫,那麼在第二次標記時,objA會被移出「即將回收」集合。以後,對象會再次出現沒有引用存在的狀況。在這個狀況下,finalize方法不會被再次調用,對象會直接變成不可觸及的狀態,也就是說,一個對象的finalize方法只會被調用一次。

代碼測試可復活的對象

/**
 * 測試Object類中finalize()方法,即對象的finalization機制。
 *
 */
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();
        }
    }
}
複製代碼

控制檯輸出數據庫

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

2.4 MAT與JProfiler的GC Roots溯源

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

獲取dump文件

方式1: 命令行使用jmapbash

  • jps
  • jmap -dump:format=b,live,file=test1.bin {進程id}

方式2:使用JVisualVM導出

  • 捕獲的heap dump文件是一個臨時文件,關閉JVisua1VM後自動刪除,若要保留,須要將其另存爲文件。
  • 可經過如下方法捕獲heap dump:
    • ➢在左側「Application」(應用程序)子窗口中右擊相應的應用程序,選擇Heap Dump(堆Dump)。
    • ➢在Monitor (監視)子標籤頁中點擊Heap Dump (堆Dump)按鈕。
  • 本地應用程序的Heap dumps做爲應用程序標籤頁的一個子標籤頁打開。同時, heap dump在左側的Application (應用程序)欄中對應一個含有時間戳的節點。右擊這個節點選擇save as (另存爲)便可將heap dump保存到本地。 7

GC Roots分析

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("結束");
    }
}
複製代碼

使用MAT查看GC Roots 8

使用jProfiler進行GC溯源 9

使用Jprofiler分析OOM

/**
 * -Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
 *
 */
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();
        }
    }
}
複製代碼

控制檯輸出

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid45386.hprof ...
Heap dump file created [7390812 bytes in 0.019 secs]
count = 6
java.lang.OutOfMemoryError: Java heap space
	at com.dsh.jvm.gc.algorithm.HeapOOM.<init>(HeapOOM.java:12)
	at com.dsh.jvm.gc.algorithm.HeapOOM.main(HeapOOM.java:20)
複製代碼

對應count=6 10

出現OOM的代碼 11

2.5 清除階段:法1_標記-清除算法

  當成功區分出內存中存活對象和死亡對象後,GC接下來的任務就是執行垃圾回收,釋放掉無用對象所佔用的內存空間,以便有足夠的可用內存空間爲新對象分配內存.
  目前在JVM中比較常見的三種垃圾收集算法是標記一清除算法( Mark一Sweep)、複製算法(Copying)、標記一壓縮算法(Mark一Compact)

背景:

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

執行過程:

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

  • 標記: Collector從引用根節點開始遍歷,標記全部被引用的對象。==通常是在對象的Header中記錄爲可達對象==。
  • 清除: Collector對堆 內存從頭至尾進行線性的遍歷,若是發現某個對象在其Header中沒有標記爲可達對象,則將其回收。 12

缺點

  • ➢效率不算高
  • ➢在進行Gc的時候,須要中止整個應用程序,致使用戶體驗差
  • ➢==這種方式清理出來的空閒內存是不連續的,產生內存碎片==。須要維護一個空閒列表

注意:何爲清除?

  • 這裏所謂的清除並非真的置空,而是把須要清除的對象地址保存在空閒 的地址列表裏。下次有新對象須要加載時,判斷垃圾的位置空間是否夠,若是夠,就存放。

2.6 清除階段:法2_複製算法

背景:

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

核心思想:

將活着的內存空間分爲兩塊,每次只使用其中一塊,在垃圾回收時將正在.使用的內存中的存活對象複製到未被使用的內存塊中,以後清除正在使用的內存塊中的全部對象,交換兩個內存的角色,最後完成垃圾回收。
堆中S0和S1使用的就是複製算法
13

優勢:

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

缺點:

  • 此算法的缺點也是很明顯的,就是須要兩倍的內存空間。
  • 對於G1這種分拆成爲大量region的GC,複製而不是移動,意味着GC須要維護region之間對象引用關係,無論是內存佔用或者時間開銷也不小。
    特別的 若是系統中的垃圾對象不少,複製算法不會很理想,複製算法須要複製的存活對象數量並不會太大,或者說很是低才行。

應用場景:

在新生代,對常規應用的垃圾回收,一次一般能夠回收708一 99的內存空間。回收性價比很高。因此如今的商業虛擬機都是用這種收集算法回收新生代。 14

2.7 清除階段:法3_標記-壓縮(整理,Mark-Compact)算法

背景:

  複製算法的高效性是創建在存活對象少、垃圾對象多的前提下的。這種狀況在新生代常常發生,可是在老年代,更常見的狀況是大部分對象都是存活對象。若是依然使用複製算法,因爲存活對象較多,複製的成本也將很高。所以,基於老年代垃圾回收的特性,須要使用其餘的算法。
  標記一清除算法的確能夠應用在老年代中,可是該算法不只執行效率低下,並且在執行完內存回收後還會產生內存碎片,因此JVM的設計者須要在此基礎之上進行改進。==標記一壓縮(Mark一Compact) 算法由此誕生==。
  1970年先後,G. L. Steele 、C. J. Chene和D.S. Wise 等研究者發佈標記一壓縮算法。在許多現代的垃圾收集器中,人們都使用了標記一壓縮算法或其改進版本。

執行過程:

  • 第一階段和標記一清除算法同樣,從根節點開始標記全部被引用對象.

  • 第二階段將全部的存活對象壓縮到內存的一端,按順序排放。

  • 以後,清理邊界外全部的空間。
    15

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

  • 兩者的本質差別在於標記一清除算法是一種非移動式的回收算法,標記一壓.縮是移動式的。是否移動回收後的存活對象是一項優缺點並存的風險決策。

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

指針碰撞(Bump the Pointer )

若是內存空間以規整和有序的方式分佈,即已用和未用的內存都各自一邊,彼此之間維繫着一個記錄下一次分配起始點的標記指針,當爲新對象分配內存時,只須要經過修改指針的偏移量將新對象分配在第一個空閒內存位置上,這種分配方式就叫作指針碰撞(Bump the Pointer) 。

優勢

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

缺點

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

2.8 小結

  • 效率上來講,複製算法是當之無愧的老大,可是卻浪費了太多內存。
  • 而爲了儘可能兼顧上面提到的三個指標,標記一整理算法相對來講更平滑一些,可是效率.上不盡如人意,它比複製算法多了一個標記的階段,比標記一清除多了一個整理內存的階段。
Mark-Sweep Mark-Compact Copying
速度 中等 最慢 最快
空間開銷 少(但會堆積碎片) 少(不堆積碎片) 一般須要活對象的2倍大小(不堆積碎片)
移動對象

2.9 分代收集算法

難道就沒有一種最優的算法麼?
==沒有最好的算法,只有更合適的算法==
  前面全部這些算法中,並無一種算法能夠徹底替代其餘算法,它們都具備本身獨特的優點和特色。分代收集算法應運而生。
  分代收集算法,是基於這樣一個事實:不一樣的對象的生命週期是不同的。所以,==不一樣生命週期的對象能夠採起不一樣的收集方式,以便提升回收效率==。通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色使用不一樣的回收算法,以提升垃圾回收的效率。
  在Java程序運行的過程當中,會產生大量的對象,其中有些對象是與業務信息相關,好比Http請求中的Session對象、線程、Socket鏈接, 這類對象跟業務直接掛鉤,所以生命週期比較長。可是還有一些對象,主要是程序運行過程當中生成的臨時變量,這些對象生命週期會比較短,好比: String對象, 因爲其不變類的特性,系統會產生大量的這些對象,有些對象甚至只用一次便可回收。

  目前幾乎全部的GC都是採用分代收集(Generational Collecting) 算法執行垃圾回收的。   在HotSpot中,基於分代的概念,GC所使用的內存回收算法必須結合年輕代和老年代各自的特色。

  • 年輕代(Young Gen)

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

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

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

2.10 增量收集算法、分區算法

增量收集算法

上述現有的算法,在垃圾回收過程當中,應用軟件將處於一種stop the World的狀態。在Stop the World狀態下,應用程序全部的線程都會掛起,暫停一切正常的工做,等待垃圾回收的完成。若是垃圾回收時間過長,應用程序會被掛起好久,將嚴重影響用戶體驗或者系統的穩定性。爲了解決這個問題,即對實時垃圾收集算法的研究直接致使了增量收集(Incremental Collecting) 算法的誕生。

基本思想

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

缺點:

使用這種方式,因爲在垃圾回收過程當中,間斷性地還執行了應用程序代碼,因此能減小系統的停頓時間。可是,由於線程切換和上下文轉換的消耗,會使得垃圾回收的整體成本上升,形成系統吞吐量的降低。

分區算法

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

寫在最後

注意,這些只是基本的算法思路,實際GC實現過程要複雜的多,目前還在發展中的前沿GC都是複合算法,而且並行和併發兼備。



JVM學習代碼及筆記(陸續更新中...)

【代碼】
github.com/willShuhuan…
【筆記】
JVM_01 簡介
JVM_02 類加載子系統
JVM_03 運行時數據區1- [程序計數器+虛擬機棧+本地方法棧]
JVM_04 本地方法接口
JVM_05 運行時數據區2-堆
JVM_06 運行時數據區3-方法區
JVM_07 運行時數據區4-對象的實例化內存佈局與訪問定位+直接內存
JVM_08 執行引擎(Execution Engine)
JVM_09 字符串常量池StringTable
JVM_10 垃圾回收1-概述+相關算法
JVM_11 垃圾回收2-垃圾回收相關概念
JVM_12 垃圾回收3-垃圾回收器

相關文章
相關標籤/搜索