JVM垃圾回收原理:標記回收對象,四種引用,垃圾收集算法,垃圾收集器

本文主要爲《深刻理解Java虛擬機》第三章的讀書記錄筆記,同時伴有一些網絡上資料的總結。html

1. 標記回收對象-對象已死?

Java堆是JVM主要的內存管理區域,裏面存放着大量的對象實例和數組。在垃圾回收算法和垃圾收集器以前,首先要作的就是判斷哪些對象已經「死去」,須要進行回收即不可能再被任何途徑使用的對象。java

1.1 引用計數法

引用計數法是這樣:給對象中添加一個引用計數器,每當有一個地方使用它時,計數器值就加1。當引用失效時,計數器就減1。任什麼時候刻計數器爲0的對象就是不可能再被使用的。算法

如今主流的Java虛擬機都沒有使用引用計數法,最主要的緣由就是它很難解決對象之間互相循環引用的問題數組

1.2 可達性分析

可達性分析的基本思路:經過一系列稱爲"GC Roots"的對象做爲起點,從這些節點開始向下搜索,若是從GC Roots到一個對象不可達,則證實此對象是不可用的,以下圖所示。瀏覽器

image.png

Java語言中,可做爲GC Roots的對象包括下面幾種:緩存

  1. 虛擬機棧(棧幀中的本地變量表)中引用的對象
  2. 本地方法棧JNI(即通常說的Native方法)引用的對象
  3. 方法區中類靜態常量引用的對象
  4. 方法區中常量引用的對象

對於Java程序而言,對象基本都位於堆內存中,簡單來講GC Roots就是有被堆外區域引用的對象。bash

2. 四種引用

JDK 1.2之前的版本中,若一個對象不被任何變量引用,那麼程序就沒法再使用這個對象。也就是說,只有對象處於(reachable)可達狀態,程序才能使用它。網絡

JDK 1.2版本開始,對象的引用被劃分爲4種級別,從而使程序能更加靈活地控制對象的生命週期。這4種級別由高到低依次爲:強引用軟引用弱引用虛引用多線程

2.1 強引用(StrongReference)

強引用是使用最廣泛的引用,以下的方式就是強引用:併發

Object strongReference = new Object();
複製代碼
  1. 若是一個對象具備強引用,那垃圾回收器絕不會回收它。直到強引用的對象不使用或者超出對象的生命週期範圍。則GC認爲該對象不存在引用,這時候就能夠回收這個對象。
  2. 當內存不足時,JVM寧願拋出OutOfMemoryError的錯誤,使程序異常終止,也不會靠隨意回收具備強引用對象來解決內存不足的問題。

舉例來講,

  1. 以下圖在一個方法內部具備一個強引用,這個引用保存在虛擬棧的棧幀中,而真正的引用內容Object則保存在Java堆中。當這個方法運行完成後,退出方法棧。這個對象再也不被GC Roots可達,那麼這個對象在下次GC時就會被回收。
public void test() {
        Object strongReference = new Object();
        // 省略其餘操做
    }
複製代碼
  1. 以下圖一個類的靜態變量須要一個強引用,這個引用保存在方法區中,而真正的引用內容Object則保存在Java堆中。當將這個引用手動制空strongReference = null後。這個對象再也不被GC Roots可達,那麼這個對象在下次GC時就會被回收。
class Obj {
    pulic static Object strongReference = new Object();
}
複製代碼

2.2 軟引用(SoftReference)

若是對象具備軟引用,則

  1. 內存空間充足時,垃圾回收器不會回收
  2. 內存空間不足時,就會嘗試回收這些對象。只要垃圾回收器沒有回收它,該對象就能夠被程序使用
// 強引用
    String strongReference = new String("abc");
    String str = new String("abc");
    // 軟引用
    SoftReference<String> softReference = new SoftReference<String>(str);
複製代碼

軟引用能夠和一個引用隊列(ReferenceQueue)聯合使用。若是軟引用的對象被垃圾回收,JVM就會把這個軟引用加入到與之關聯的引用隊列中

ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
    // 強引用
    String str = new String("abc");
    SoftReference<String> softReference = new SoftReference<>(str, referenceQueue);
	// 消除強引用
    str = null;
    // Notify GC
    System.gc();

    System.out.println(softReference.get()); // abc

    Reference<? extends String> reference = referenceQueue.poll();
    System.out.println(reference); //null
複製代碼

注意:

  1. 軟引用對象是在JVM內存不夠的時候纔會被回收,咱們調用System.gc()方法只是起通知做用,JVM何時掃描回收對象是JVM本身的狀態決定的。
  2. 就算掃描到軟引用對象真正開始GC也不必定會回收它,只有內存不夠的時候纔會回收。
  3. 軟引用對象回收規則適應的前提條件是這個對象只有軟引用。因此在上面的用例中要把強引用清除。

也就是說,垃圾收集線程會在虛擬機拋出OutOfMemoryError以前回收軟引用對象,並且虛擬機會盡量優先回收長時間閒置不用的軟引用對象。對那些剛構建的或剛使用過的較新的軟對象會被虛擬機儘量保留

應用場景:

瀏覽器的後退按鈕。按後退時,這個後退時顯示的網頁內容是從新進行請求仍是從緩存中取出呢?這就要看具體的實現策略了。

  1. 若是一個網頁在瀏覽結束時就進行內容的回收,則按後退查看前面瀏覽過的頁面時,須要從新構建;
  2. 若是將瀏覽過的網頁存儲到內存中會形成內存的大量浪費,甚至會形成內存溢出。

這時候就可使用軟引用,很好的解決了實際的問題:

// 獲取瀏覽器對象進行瀏覽
   Browser browser = new Browser();
   // 從後臺程序加載瀏覽頁面
   BrowserPage page = browser.getPage();
   // 將瀏覽完畢的頁面置爲軟引用
   SoftReference softReference = new SoftReference(page);
   // 消除強引用
   page = null;
   
   // 回退或者再次瀏覽此頁面時
   if(softReference.get() != null) {
       // 內存充足,尚未被回收器回收,直接獲取緩存
       page = softReference.get();
   } else {
       // 內存不足,軟引用的對象已經回收
       page = browser.getPage();
       // 從新構建軟引用
       softReference = new SoftReference(page);
   }
複製代碼

2.3 弱引用(WeakReference)

相比較軟引用,具備弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它鎖管轄的內存區域的過程當中,一旦發現了具備弱引用的對象,無論當前內存空間足夠與否,都會回收它的內存。不過,因爲垃圾回收器是一個優先級很低的線程,所以不必定會很快發現那些只具備弱引用的對象。

String str = new String("abc");
    WeakReference<String> weakReference = new WeakReference<>(str);
    // 消除強引用
    str = null;
複製代碼

一樣,弱引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是弱引用的對象被垃圾回收,JVM就會把這個弱引用加入到與之關聯的引用隊列中

ReferenceQueue<String> queue = new ReferenceQueue<>();
        String str = new String("abc");
        WeakReference<String> weakReference = new WeakReference<>(str, queue);
        str = null;
        System.gc();
        try {
            // 休息幾分鐘,等待上面的垃圾回收線程運行完成
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(weakReference.get());  // null
        System.out.println(queue.poll());  // java.lang.ref.WeakReference@22a71081
複製代碼

2.4 虛引用(PhantomReference)

虛引用顧名思義,就是形同虛設。與其餘幾種引用都不一樣,虛引用並不會決定對象的生命週期。若是一個對象僅持有虛引用,那麼它就和沒有任何引用同樣,在任什麼時候候均可能被垃圾回收器回收。

應用場景:

虛引用主要用來跟蹤對象被垃圾回收器回收的活動。 虛引用軟引用弱引用的一個區別在於:

虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,若是發現它還有虛引用,就會在回收對象的內存以前,把這個虛引用加入到與之關聯的引用隊列中。

String str = new String("abc");
    ReferenceQueue queue = new ReferenceQueue();
    // 建立虛引用,要求必須與一個引用隊列關聯
    PhantomReference pr = new PhantomReference(str, queue);
複製代碼

程序能夠經過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要進行垃圾回收。若是程序發現某個虛引用已經被加入到引用隊列,那麼就能夠在所引用的對象的內存被回收以前採起必要的行動。

3. 垃圾收集算法

3.1 標記-清除算法

標記-清除算法分爲「標記」和「清除」兩個階段,執行過程以下圖所示:

  1. 標記:首先標記出全部須要回收的對象
  2. 清除:在標記完成後統一回收全部被標記的對象

image.png

標記-清除算法主要有兩個不足:

  1. 效率問題,標記和清除的兩個過程效率都不高
  2. 標記-清除會產生大量不連續的內存碎片,這會致使在後面須要分配連續的大對象時,沒法找到足夠大的連續內存而致使不得不提早觸發另外一次垃圾收集動做

3.2 複製算法

複製算法的大體思路以下,其執行過程以下圖所示:

  1. 首先將可用內存分爲大小相等的兩塊,每次只使用其中的一塊。
  2. 當這一塊的內存用完了,就將還存活的對象連續複製到另外一塊上面,而後把使用過的內存空間一次清理掉

複製算法的代價就是將內存縮小爲原來的一半。

image.png

如今的商業虛擬機都是採用複製算法來回收新生代。

  1. 新生代的內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間。
  2. 每次使用Eden和一塊Survivor,當進行回收是,將Eden和Survivor中還存活的對象一次性複製到另外一個Survivor空間上。而後,清理掉Eden和剛剛使用過的Survivor空間。
  3. HotSpot虛擬機默認Eden和Survivor的大小比例爲8 : 1,這樣每次新生代可用內存爲整個新生代的90% (10% + 80%),只有10%的內存會被浪費。

3.3 標記-整理算法

標記-整理算法分爲「標記」和「整理」兩個階段,執行過程以下圖所示:

  1. 標記:首先標記出全部須要回收的對象
  2. 整理:讓全部的存活的對象都向一端移動,而後直接清除掉邊界之外的內存

image.png

3.4 分代收集算法

分代收集算法就是降Java堆分爲新生代和老年代,根據其各自的特色採用最適當的收集算法。

  1. 新生代中大批對象死去,只有少許存活,就選用複製算法
  2. 老年代中對象存活概率高,沒有額外的空間對它進行分配擔保,就必須使用標記-清除或者標記-整理算法。

4. 垃圾回收器

JVM垃圾收集器發展歷程大體能夠分爲如下四個階段: Serial(串行)收集器 -> Parallel(並行)收集器 -> CMS(併發)收集器 -> G1(併發)收集器

下圖展現了7種做用域不一樣分代的收集器,若是兩個收集器之間存在連續,就說明它們能夠搭配使用。下面逐一介紹這些收集器的特性、基本原理和使用場景。

4.1 Serial類收集器

Serial類收集器是一個單線程的收集器:

  1. 它只會用單個收集線程去進行垃圾回收的工做

  2. 它在進行垃圾收集的時候會「Stop The World」暫停其餘全部的工做表線程,直到它收集結束

  3. Serial收集器採起複製算法新生代進行單線程的回收工做

  4. Serial Old收集器採起標記-整理算法在老年代進行單線程的回收工做

image.png

4.2 Parallel類收集器

Parallel類收集器就是Serial收集器的多線程版本:

  1. 它使用多個收集線程取進行垃圾回收工做
  2. 它在進行垃圾收集的時候也會「Stop The World」暫停其餘全部的工做表線程,直到它收集結束
  3. ParNew收集器採起複製算法新生代進行多線程的回收工做
  4. Parallel Scavenge收集器也是一個新生代收集器,不過它被稱爲「吞吐量優先」收集器,它提供了2個能精確控制吞吐量的參數:
    • -XX : MaxGCPauseMillis:控制最大垃圾收集停頓時間
    • -XX : GCTimeRatio : 直接設置吞吐量大小,垃圾收集時間佔總時間的比率

Parallel Scavenge收集器還有一個開關參數-XX: UseAdaptiveSizePolicy,打開這個開關後就不用手動指定新生代的大小(-Xmn),Eden與Survivor區的比例(-XX:SurvivorRatio)等細節參數了,JVM會動態調整這些參數已提供最合適的停頓時間或者最大吞吐量。

  1. Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和標記-整理算法在老年代進行垃圾回收。

image.png

4.3 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。它是一個基於標記-清除算法實現的,運做過程分爲4個步驟:

  • 初始標記(CMS initial mark): 須要「Stop The World」,僅僅只是標記下GC Roots能直接關聯到的對象,速度很快

  • 併發標記(CMS concurrent mark): CMS線程與應用線程一塊兒併發執行,從GC Roots開始對堆中對象進行可達性分析,找出存活對象,耗時較長

  • 從新標記(CMS remark):從新標記就是爲了修正併發標記期間因用戶線程繼續運做而致使標記產生變更的那一部分對象的標記記錄,能夠多線程並行

  • 併發清除(CMS concurrent sweep):CMS線程與應用線程一塊兒併發執行,進行垃圾清除

    image.png

    CMS收集器優勢:併發收集低停頓

    CMS的三個明顯的缺點:

    1. CMS收集器對CPU的資源很是敏感。CPU的數量較少不足4個(好比2個)時,CMS對用戶程序的影響就可能變的很大。
    2. CMS收集器沒法處理浮動垃圾(Floating Carbage),可能出現"Concurrent Mode Failture"失敗而致使產生另外一次Full GC的產生。浮動垃圾就是併發清理階段,用戶線程產生的新垃圾
    3. CMS是基於標記-清除算法的,收集結束後會有大量的空間碎片,就可能會在老年代中沒法分配足夠大的連續空間而不得不觸發另外一次Full GC。

4.4 G1收集器

同優秀的CMS同樣,G1也是關注最小停頓時間的垃圾回收器,也一樣適合大尺寸堆內存,官方也推薦用G1來代替選擇CMS。

  1. G1收集器的最大特色就是引入了分區的思路,弱化了分代的概念
  2. G1從總體來看是基於標記-整理算法實現的,從局部(兩個Region之間)來看是基於複製算法實現的

4.4.1 G1相對於CMS的改進

  1. G1是基於標記-整理算法,不會產生空間碎片,在分配大的連續對象是不會由於沒法獲得連續空間而不得不提早觸發一次Full GC
  2. 停頓時間可控,G1能夠經過設置停頓時間來控制垃圾回收時間
  3. 並行與併發,G1能更充分的利用CPU,多核環境下的硬件優點來縮短stop the world的停頓時間

4.4.2 G1與CMS的區別

(1)堆內存模型的不一樣

G1以前的JVM堆內存模型,堆被分爲新生代,老年代,永久代(1.8以前,1.8以後是元空間),新生代中又分爲Eden和兩個Survivor區。

img

  1. G1收集器的堆內存模型,堆被分爲不少個大小連續的區域(Region),Region的大小能夠經過-XX: G1HeapRegionSize參數指定,大小區間爲[1M,32M]。

  2. 每一個Region被標記了E、S、O和H,這些區域在邏輯上被映射爲Eden,Survivor,老年代和巨型區(Humongous Region)。巨型區域是爲了存儲超過50%標準region大小的巨型對象。

    img

(2)應用分代的不一樣

G1能夠在新生代和老年代使用,而CMS只能在老年代使用。

(3)收集算法的不一樣

G1是複製+標記-整理算法,CMS是標記清除算法。

4.4.3 G1收集器的工做流程

G1收集器的工做流程大體分爲以下幾個步驟:

  1. 初始標記(Initial Marking): 須要「Stop The World」,僅僅只是標記下GC Roots能直接關聯到的對象,速度很快
  2. 併發標記(Concurrent Marking): G1線程與應用線程一塊兒併發執行,從GC Roots開始對堆中對象進行可達性分析,找出存活對象,耗時較長
  3. 最終標記(Final Marking): 最終標記階段則是爲了修正在併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分標記記錄,須要「Stop The World」,能夠多線程並行
  4. 篩選回收(Live Data Counting and Evacuation): 對各個Region的回收價值和成本進行排序,根據用戶所期待的GC停頓時間制定回收計劃。具體地,在後臺維護一個優先隊列,每次根據容許的收集停頓時間,優先回收價值最大的Region

4.4.4 G1的GC模式

G1提供了兩種GC模式,Young GC和Mixed GC,兩種都是徹底Stop The World的

(1)YoungGC
  1. 在分配通常對象(非巨型對象)時,當全部的Eden Region使用達到最大閾值而且沒法申請到足夠內存時,會觸發一次YoungGC。
  2. 每次YoungGC會回收全部的Eden以及Survivor區,而且將存活對象複製到Old區以及另外一部分的Survivor。
(2)MixedGC

當愈來愈多的對象晉升到老年代old region時,爲了不堆內存被耗盡,虛擬機會觸發一次mixed gc,該算法並非一個old gc,除了回收整個young region,還會回收一部分的old region。這裏須要注意:是一部分老年代,而不是所有老年代,能夠選擇哪些old region進行收集,從而能夠對垃圾回收的耗時時間進行控制

G1沒有fullGC概念,須要fullGC時,調用serialOldGC進行全堆掃描(包括eden、survivor、o、perm)。

參考與感謝

  1. 理解Java的強引用、軟引用、弱引用和虛引用
  2. 深刻剖析JVM:G1收集器+回收流程+推薦用例
  3. Java Hotspot G1 GC的一些關鍵技術
相關文章
相關標籤/搜索