天天看點

JVM——3. 垃圾回收算法1. 為什麼需要垃圾回收2. 垃圾回收算法

文章目錄

  • 1. 為什麼需要垃圾回收
    • 1.1 可達性分析算法
    • 1.2 Java中的引用:
      • 1.2.1 強引用(Strong Reference):
      • 1.2.2 軟引用(Soft Reference):
      • 1.2.3 弱引用(Weak Reference):
      • 1.2.4 虛引用(Phantom Reference):
    • 1.3 對象的真正死亡:
    • 1.4 回收方法區:
  • 2. 垃圾回收算法
    • 2.1 标記-清除算法
    • 2.2 複制算法
      • 2.2.1 複制算法的優化:Eden區和Survivor區
      • 2.2.2 優化的複制算法下Minor GC的過程
    • 2.3 标記-整理算法
    • 2.4 分代收集算法

1. 為什麼需要垃圾回收

在系統運作時,新生成的對象是優先配置設定在堆記憶體中的新生代,随着新生成的對象越來越多,新生代就會裝不下了,這個時候就需要垃圾回收來把“垃圾”對象給回收掉,釋放記憶體空間。

那在需要垃圾回收的時候,哪些對象才是“垃圾”對象呢?

在主流虛拟機中采用的是“可達性分析算法”來判斷哪些對象能夠回收,哪些對象不能回收的。

1.1 可達性分析算法

通過一系列稱為“GC Roots”的對象作為起始點,從這些節點向下搜尋,搜尋所走過的路徑稱為引用鍊(Refrence Chain),當一個對象到GC Roots沒有任何引用鍊相連(圖論的話來說,這個對象到GC Roots不可達)時,證明此對象是不可用的,即可被回收的。

另外一種表述:将一系列的GC Roots作為初始的存活對象集合(live set),然後從該集合出發,探索所有能夠被該集合引用到的對象,并将其加入到該集合,這個過程稱為标記(mark);最終未被探索到的對象便是死亡的,即可被回收的。

圖中object5,6,7到GC Roots不可達,是以會被判斷成可回收對象。

JVM——3. 垃圾回收算法1. 為什麼需要垃圾回收2. 垃圾回收算法

什麼是GC Roots?

  • 在Java語言中,可作為GC Roots的對象:

    (1). 虛拟機棧(棧幀中的本地變量表)中引用的對象;(局部變量)

    (2). 方法區中類靜态屬性引用的對象;(靜态變量)

    (3). 方法區中常量引用的對象(static final);(成員變量不行,因為成員變量是存儲在堆記憶體的對象中的,和對象共存亡)

    (4). 本地方法棧中JNI(Native方法)引用的對象;

    (5). 活着的線程;

這裡有一篇文章進行了不錯的證明: 【證】:那些可作為GC Roots的對象

1.2 Java中的引用:

傳統定義:如果reference類型的資料中存儲的數值代表的是另外一塊記憶體的起始位址,就稱這塊記憶體代表這一個引用。

JDK1.2後對引用進行了擴充,分為強引用、軟引用、弱引用、虛引用:

1.2.1 強引用(Strong Reference):

在程式中普遍存在,類似于“Object obj = new Object()”這類的引用,隻要強引用還在(即obj還指向Object對象),GC永遠不會回收被引用的對象。

當手動置空 obj=null; 這個對象在下次GC運作的時候被回收。

  • 示例:
public class Kafka {
    public static ReplicaManager replicaManager = new ReplicaManager();
}
           

1.2.2 軟引用(Soft Reference):

描述一些還有用但并非必須的對象,通常用于緩存,JDK1.2後,提供了SoftReference類來實作軟引用。

正常情況下垃圾回收是不會回收軟引用對象的;軟引用對象的回收時機:

  • 當發生GC時,JVM虛拟機取決于軟引用對象是否新建立或者最近被使用過,來判斷是否回收;
  • 在JVM虛拟機發生OOM異常時,所有軟引用對象都會被回收;
  • 如果軟引用對象被一個強引用指向,即使發生OOM時,也不會被回收;
  • 示例:
public class Kafka {
    public static SoftReference<ReplicaManager> replicaManager = new SoftReference<ReplicaManager>(ReplicaManager());
}
           
  • 回收時機:

    clock - timestamp <= freespace * SoftRefLRUPolicyMSPerMB

    • clock - timestamp:軟引用對象多久沒有被通路過了;
    • freespace(MB):JVM中的空閑記憶體空間;
    • SoftRefLRUPolicyMSPerMB(JVM參數):每MB的空閑記憶體允許SoftReference對象存活多久;
  • 示例:
    • 目前JVM裡的空閑空間有1000M,SoftRefLRUPolicyMSPerMB 預設值為1000毫秒,則現在允許 SoftReference對象存活 1000*1=1000秒;

1.2.3 弱引用(Weak Reference):

描述一些非必須對象,強度比軟引用更弱一點,被弱引用關聯的對象隻能生存到下一次垃圾回收發生之前(即跟沒有引用一樣,隻要發生垃圾回收,都會把這個對象回收掉)。目前GC工作時,無論記憶體是否足夠,都會回收掉弱引用關聯的對象。JDK1.2後,提供了WeakReference類來實作軟引用。

  • 示例:
public class WeakReferenceDemo {

    public static WeakReference<String> weakReference1;

    public static void main(String[] args) {

        test1();
        //可以輸出hello值;因為此時已無強引用指向對象,但是未進行gc,是以弱引用仍持有對象
        System.out.println("未進行gc時,隻有弱引用指向value記憶體區域:" + weakReference1.get());

        //執行GC,回收弱引用
        System.gc();

        //弱引用被釋放掉了,是以引用的對象也被回收了
        System.out.println("進行gc時,隻有弱引用指向value記憶體區域:" + weakReference1.get());

    }

    public static void test1() {
        String hello = new String("value");

        weakReference1 = new WeakReference<>(hello);

        System.gc();
        //此時gc不會回收弱引用,因為字元串"value"仍然被hello對象強引用
        System.out.println("進行gc時,強引用與弱引用同時指向value記憶體區域:" + weakReference1.get());

    }
}

// output:
進行gc時,強引用與弱引用同時指向value記憶體區域:value
未進行gc時,隻有弱引用指向value記憶體區域:value
進行gc時,隻有弱引用指向value記憶體區域:null
           

說明:

  • 當有強引用指向value的時候,即使進行GC,弱引用不會被釋放,引用的對象不會被回收;
  • 當沒有強引用指向value的時候,此時執行GC,弱引用會被釋放,引用的對象會被回收;

WeakHashMap:

  • 介紹:WeakHashMap的鍵是弱鍵,當某個鍵不再正常使用時,會被從WeakHashMap中自動移除;即對于一個給定的鍵,其映射的存在并不阻止垃圾回收器對該鍵的丢棄,這就使該鍵成為可終止的,然後被回收,某個鍵被回收時,它對應的鍵值對也就從映射中有效地移除了。
  • 實作原理:通過WeakReference和ReferenceQueue實作,WeakHashMap的key是弱鍵,即是WeakReference類型的,ReferenceQueue是一個隊列,它會儲存被GC回收的弱鍵。
    • 建立WeakHashMap,将鍵值對添加到WeakHashMap中。WeakHashMap是通過數組table儲存Entry(單向連結清單的鍵值對);
    • 當某弱鍵不再被其他對象引用并被GC回收時,這個弱鍵同時會被添加到ReferenceQueue中;
    • 當下一次需要操作WeakHashMap時,會先同步table和queue,table中儲存了全部的鍵值對,queue中儲存了已經被GC回收的鍵值對;同步它們,即是删除table中廢棄的鍵值對;

1.2.4 虛引用(Phantom Reference):

最弱的一種引用關系,對象是否有虛引用的存在,完全不會對生存時間夠成影響,也無法通過虛引用來取得一個對象執行個體。

唯一目的是能在對象被GC回收時收到一個系統通知(這就可以實作在對象被回收時做一些事情了:如DirectByteBuffer的Cleaner機制);JDK1.2後,提供了PhantomReference類來實作虛引用。

1.3 對象的真正死亡:

在可達性分析算法中不可達的對象(沒有GC Roots引用的對象),是一定馬上就被回收嗎?

不是的,一個對象真正的死亡,還要經過兩次标記的過程:

  • 如果對象在可達性分析後發現沒有與GC Roots相連接配接的引用鍊,那它會被第一次标記并且進行一次篩選,篩選的條件是此對象是否還有必要執行 finalize() 方法;沒有必要執行的條件是:
    • 對象沒有覆寫finalize()方法;
    • finalize()方法 已經被虛拟機調用過;
  • 如果這個對象有必要執行finalize()方法(實作了finalize()方法),這個對象會被放置在一個F-Queue隊列中,并由一個低優先級的Finalizer線程執行它。
    • finalize()方法是對象逃脫死亡的最後一次機會,如果對象要在finalize()中拯救自己,隻需要重新與引用鍊上的任何一個對象建立關聯即可,比如把自己(this)指派給某個類變量或者對象成員變量。那在第二次标記時會被移出F-Queue隊列中。
      • 示例:
      public class ReplicaManager {
          public static ReplicaManager instance;
          @override
          protected void finalize() throws Throwable {
              ReplicaManager.instance = this;
          }
      }
                 
    • 如果對象還沒有在此方法中逃脫,就會被回收了;

      由于此方法運作代價高昂,不确定性大,無法保證各個對象的調用順序,是以完全不推薦使用此方法來拯救對象。

  • 是以也可以在finalize()方法中,實作一些在對象回收前的自定義操作;(Java官方不推薦)

1.4 回收方法區:

方法區(如HotSpot虛拟機中的元空間或者永久代)的垃圾回收主要回收兩部分内容:廢棄常量和無用的類。

  • 回收廢棄常量:與回收Java堆中的對象非常類似。以常量池中的字面量的回收 為例,例如一個字元串“abc”已經進入了常量池中,但是目前系統中沒有任何String對象是“abc”,也就是說沒有任何String對象引用常量池中的這個“abc”對象,當這個時候發生垃圾回收,必要的話,這個常量就會被清理出常量池。常量池中的其他類(接口)、方法、字段的符号引用都是這樣。
  • 回收無用的類:條件比判斷“廢棄常量”複雜許多,需要同時滿足下面三個條件:
    • 該類所有的執行個體都已經被回收,也就是Java堆中不存在該類的任何執行個體;
    • 加載該類的ClassLoader都已被回收;
    • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射通路該類的方法。

在大量使用反射,動态代理,CGLIB等ByteCode架構、動态生成JSP以及OSGI這類頻繁自定義ClassLoader的場景都需要虛拟機具備類解除安裝功能,以保證永久代不會溢出。

2. 垃圾回收算法

2.1 标記-清除算法

标記-清除算法分為兩個二個階段:首先标記出所有需要回收的對象;在标記完成後統一回收所有被标記的對象。

JVM——3. 垃圾回收算法1. 為什麼需要垃圾回收2. 垃圾回收算法
  • 缺點:
    • 空間問題:标記清除之後會産生大量不連續的記憶體碎片,導緻以後程式運作過程需要配置設定較大對象的時候,無法找到足夠連續的記憶體,而不得不提前觸發垃圾回收動作;
    • 配置設定效率低:如果是連續的記憶體空間,可以通過指針加法的方式來做配置設定;而對于不連續的空閑清單,JVM需要逐個通路清單中的項來查找能夠放入建立對象的空閑記憶體;

2.2 複制算法

将可用記憶體按照容量劃分為大小相等的兩塊,每次隻使用其中的一塊。當這一塊的記憶體用完了需要進行回收時,就标記出需要存活的對象,然後将這些需要存活的對象複制到另外一塊空白記憶體中(複制的時候可以緊湊地連續排列),然後把已使用的記憶體空間一次性清理掉;現在的商業虛拟機都是采用這種複制算法來回收新生代。

JVM——3. 垃圾回收算法1. 為什麼需要垃圾回收2. 垃圾回收算法
  • 優點:每次對整個半區進行記憶體回收,不會導緻記憶體碎片的問題,實作簡單,運作高效;
  • 缺點:将記憶體縮小為原來的一半,對記憶體的使用效率太低;

2.2.1 複制算法的優化:Eden區和Survivor區

根據IBM公司的研究表明:新生代中的對象98%都是“朝生夕死”的(即存活時間很短,存活對象占比很小),是以并不需要按照1:1的比例來劃分記憶體空間,而是将記憶體劃分為一塊較大的Eden區和兩塊較小的Survivor區,每次使用Eden區和一塊Survivor區(用于存放上次Minor GC後還存活的對象)。HotSpot虛拟機預設Eden區和Survivor區的大小是8:1,這樣每次新生代中的可用記憶體空間隻有10%會被浪費。

  • 示例(假設Eden區有800M記憶體,則相當于有900M的記憶體可以使用):
    JVM——3. 垃圾回收算法1. 為什麼需要垃圾回收2. 垃圾回收算法
    當發生回收時:将Eden區和剛才使用的Survivor區還存活着的對象(此時存活的對象非常少)一次性的複制到另外一塊Survivor空間上,然後清理掉Eden區和使用過的Survivor區。
    JVM——3. 垃圾回收算法1. 為什麼需要垃圾回收2. 垃圾回收算法
    接着,新對象繼續配置設定在Eden區,另外那塊Survivor區繼續儲存Minor GC後存活的對象,并始終保持有一塊Survivor區是空的,這樣一直循環使用三塊記憶體區域。
  • 為什麼需要兩塊 Survivor區?
    • 假設隻有一塊 Survivor區:
      • 當發生第一次YGC的時候,回收Eden區,并将存活對象被放入了這塊Survivor區;
      • 在發生下一次YGC時,回收Eden區和這塊Survivor區,此時Eden區和這塊Survivor區都可能有存活對象;
        • 第一,如果此處不做處理,等到後面幾次YGC後,Survivor區不夠了才移到老年代;多次的進入Survivor區,會導緻記憶體區域不連續,記憶體使用率下降;
        • 第二,如果直接将Survivor區的存活對象移到老年代,将會導緻老年代空間迅速增大,并且這些對象很可能下一次就該被回收的;(也就是說老年代放入了很多低年齡的對象)
        • 是以如果有兩塊Survivor區的話,可以保證記憶體區域連續,并且在Survivor區中對象的互相複制,可以增加年齡,實作年齡門檻值判斷;

當然,我們沒有辦法保證每次回收都隻有不多于10%的對象存活,當另外的Survivor空間不夠時,就需要依賴其他記憶體(老年代)進行配置設定擔保。如果另外一塊Survivor沒有足夠的空間存放上一次新生代收集下來的存活對象時,這些對象将直接通過配置設定擔保機制進入老年代。

2.2.2 優化的複制算法下Minor GC的過程

  • GC開始時,對象隻會存在于Eden區和From Survivor區,To Survivor區是空的(作為保留區域)。
  • GC進行時,Eden區中所有存活的對象都會被複制到To Survivor區,而在From Survivor區中,仍存活的對象會根據它們的年齡值決定去向,年齡值達到年齡門檻值(預設為15,JVM參數“-XX:MaxTenuringThreshold”)的對象會被移到老年代中,沒有達到門檻值的對象會被複制到To Survivor區(每複制一次,年齡加1);接着清空Eden區和From Survivor區,新生代中存活的對象都在To Survivor區。
  • 接着,From Survivor區和To Survivor區會進行交換,把To Survivor區中的對象複制到From Survivor區中,也就是新的From Survivor區就是上次GC的To Survivor區。總之,不管怎樣都會保證To Survivor區在一輪GC之後是空的。
  • GC時當To Survivor區沒有足夠的空間存放新生代存活的對象時,需要依賴老年代進行配置設定擔保,将這些對象存放在老年代中。

2.3 标記-整理算法

複制收集算法在對象存活率較高時就要進行較多的複制操作,效率将會變低,另外還需要額外的空間進行擔保,以應對使用的記憶體中所有對象都100%存活的極端情況,是以在老年代一般不能直接使用此算法。

“标記-整理”算法的标記過程仍然與“标記-清除”算法一樣,但後續步驟不是直接對可回收的對象進行清理,而是讓所有存活的對象都向一端移動(壓縮),然後清理掉端邊界以外的記憶體。

JVM——3. 垃圾回收算法1. 為什麼需要垃圾回收2. 垃圾回收算法

缺點:

  • 壓縮算法的性能開銷;

HotSpot虛拟機中CMS垃圾回收器回收老年代采用的就是 标記-整理算法。

2.4 分代收集算法

目前商業虛拟機的垃圾收集都采用“分代收集”算法,根據對象存活周期的不同将記憶體劃分為幾塊。一般是把Java堆劃分為新生代和老年代,再根據各個年代的特點來采用最适合的收集算法。

在新生代中,每次垃圾收集都發現有大量對象死去,隻有少量存活,就采用複制算法,隻需要付出少量存活對象的複制成本就可以完成收集;而老年代中因為對象存活率較高,沒有額外空間進行配置設定擔保,就必須使用“标記-清除”算法或者“标記-整理”算法來進行回收。