深入理解GC 回收機制與分代回收策略

垃圾回收(Garbage Collection,GC) 指的是JVM的自動內存管理機制,即:當堆內存中分配的對象所占的內存不再被引用時,就會觸發JVM自動釋放內存以讓出對象所占用的存儲空間,雖然 Java 不用手動管理內存回收,代碼寫起來很順暢。但是你有沒有想過,這些內存是怎么被回收的?

其實,JVM 是有專門的線程在做這件事情。當我們的內存空間達到一定條件時,會自動觸發這個過程。而這個過程就叫作 GC,負責 GC 的組件,就叫作垃圾回收器。

按照語義上的意思,垃圾回收,首先就應該找到這些垃圾,然后回收掉。但是在GC 過程正好相反,它是先找到活躍的對象,然后把其他不活躍的對象判定為垃圾,然后清理掉。所以垃圾回收只與活躍的對象有關,和堆的大小無關,即:GC過程是逆向的??傮w就只有以下幾點:

  • 什么是垃圾?
  • 如何識別內存中該對象是垃圾?
  • 何時回收釋放垃圾占用的內存(何時觸發GC)?
  • 如何垃圾回收(垃圾收集算法)?

什么是垃圾?

垃圾 顧名思義就是JVM分配的內存不再被引用,即對象不在GCRoot引用鏈上了。 我們已經知道垃圾是什么了,但是JVM是如何識別該對象是垃圾?

如何識別該對象是垃圾?

引用計數算法(Reference Counting)

引用計數算法 就是在分配對象時,會額外為對象分配一段空間,用于記錄指向該對象的引用個數。如果有一個新的引用指向該對象,則計數器加 1;當一個引用不再指向該對象,則計數器減 1 。當計數器的值為 0 時,則該對象為垃圾對象,我記得在Glide中正在被使用的ActivityResoure就是用的這個算法。

不過引用計數算法存在一個致命問題就是無法回收循環引用對象。當有兩個對象相互引用時,由于它們互相引用對方所以計數都不為零,這就會導致這兩個對象無法回收。所以,JVM采用的是另一種算法來判斷對象是否存活:可達性分析算法。

可達性分析算法(Reachability analysis algorithm)

可達性分析算法是從離散數學中的圖論引入的,JVM 把堆內存中所有的對象之間的引用關系看作一張圖,從GC Root對象為起始點,從這些節點開始搜索,搜索所走過的路徑稱為 引用鏈,在引用鏈上的對象就存活,而不在引用鏈上的對象就認定為可回收對象。如下圖所示:

gcRoot.png

圖中的ABCD/E對象引用與GC Root對象引用之間都存在一條直接或者間接的引用鏈,這也就說明它們與 GC Root引用鏈上是可達的,因此它們不是垃圾,是不能被GC回收掉的。而對象M和K雖然被對J 引用到,但是并不存在一條引用鏈連接它們與 GC Root,所以當 GC 進行垃圾回收時,只要遍歷到 J/K/M 這 3 個對象,就會將它們回收。

注意:上圖中圓形圖標雖然標記的是對象,但實際上代表的是此對象在內存中的引用。包括 GC Root 也是一組引用而并非對象。這里的引用就好比C語言中內存地址。

什么GC Root 對象,哪些對象可以作為GC Root 對象引用?一般情況有以下幾種,下面會通過代碼去做驗證:

1)、Java 虛擬機棧(局部變量表)中的引用的對象(正在運行的方法使用到的變量、參數等)。
2)、方法區中靜態引用屬性引用的對象(static關鍵字聲明的字段)。
3)、仍處于存活狀態中的線程對象。
4)、本地方法棧中引用的對象(Native 方法中 JNI 引用的對象)。

下面我們來通過代碼驗證能夠作為GC Root的幾種情況:
注意:為了驗證能夠作為GC Root的引用對象,從物理內存中分配出 200M 空間分配給 JVM 內存:java -Xms200m HelloWorld,需要記住的是我們驗證的是能夠作為GC Root,而GC Root就是當GC觸發也不會被回收。

1)、Java 虛擬機棧(局部變量表)中的引用的對象(,正在運行的方法使用到的變量、參數)。

public class GCRootLocalVariable {
  private byte[] memory = new byte[100 * 1024 * 1024];

  public static void main(String[] args) {
      System.out.println("Start:");
      printMemory();
      method();
      System.gc();
      System.out.println("Second GC finish");
      printMemory();
}

  public static void method() {
      GCRootLocalVariable g = new GCRootLocalVariable();
      System.gc();
      System.out.println("First GC finish");
      printMemory();
  }

  /**
   * 打印出當前JVM剩余空間和總的空間大小
   */
  public static void printMemory() {
      System.out.print("free is " + Runtime.getRuntime().freeMemory() / 1024 / 1024 + " M, ");
      System.out.println("total is " + Runtime.getRuntime().totalMemory() / 1024 / 1024 + " M, ");
      }
  }

打印日志:

Start:
free is 189 M, total is 192 M, 
First GC finish
free is 90 M, total is 192 M, 
Second GC finish
free is 190 M, total is 192 M, 

結論:
(1)當第一次 GC時,即:System.gc()時,g 作為局部變量,引用了 new 出的對象(100M),并且g作為 GC Roots,在 GC 后并不會被 GC 回收。
(2)當第二次 GC:method() 方法執行完后,局部變量 g 跟隨方法銷毀,不再有引用類型指向該 100M 對象,所以第二次 GC 后此 100M 也會被回收。

注意:上面日志包括后面的實例中,因為有中間變量,所以會有 1M 左右的誤差,但不影響我們分析 GC 過程。

2)、方法區中靜態引用屬性引用的對象,static關鍵字聲明的字段。

public class GCRootStaticVariable {
   private static int _10MB = 10 * 1024 * 1024;
   private byte[] memory;
   private static GCRootStaticVariable staticVariable;
   GCRootStaticVariable memberVariable;

  public GCRootStaticVariable(int size) {
      memory = new byte[size];
  }

  public static void main(String[] args) {
      System.out.println("Start:");
      printMemory();
      GCRootStaticVariable g = new GCRootStaticVariable(4 * _10MB);
      g.memberVariable = new GCRootStaticVariable(2 * _10MB);
      GCRootStaticVariable.staticVariable = new GCRootStaticVariable(8 * _10MB);
      // 將g置為null, 調用GC時可以回收此對象內存
      g = null;
      System.out.println("Start GC before");
      printMemory();
      System.gc();
      System.out.println("GC Finish after");
      printMemory();
    }

  public static void printMemory() {
      System.out.print("free is " + Runtime.getRuntime().freeMemory() / 1024 / 1024 + " M, ");
      System.out.println("total is " + Runtime.getRuntime().totalMemory() / 1024 / 1024 + " M, ");
  }
}

打印日志:

Start:
free is 189 M, total is 192 M, 
Start GC before
free is 50 M, total is 192 M, 
GC Finish after
free is 110 M, total is 192 M,

結論:
程序剛開始運行時內存為 189M,并分別創建了 g 對象(40M),同時也初始化 g 對象內部的靜態變量 staticVariable 對象(80M)和g對象成員變量(20M)memberVariable 。當調用 GC 時,只有 g 對象(40M) 和g成員變量memberVariable 被 GC 回收掉,因為memberVariable 并不是GC Root,而他的GC Root(g)已經被置為了null ,而靜態變量 staticVariable 作為 GC Root,它引用的 80M 并不會被回收。靜態變量作為類變量是隨著類生命周期為存在的,

3)、仍處于存活狀態中的線程對象。

public class GCRootThread {
private int _10MB = 10 * 1024 * 1024;
private byte[] memory = new byte[8 * _10MB];

public static void main(String[] args) throws Exception {
    System.out.println("開始前內存情況:");
    printMemory();
    AsyncTask at = new AsyncTask(new GCRootThread());
    Thread thread = new Thread(at);
    thread.start();

   (1) at = null; // 并沒有會回收掉,所以證明 驗證活躍線程可以作為GC Root
    System.gc();
    System.out.println("main方法執行完畢,完成GC");
    printMemory();

    thread.join();
   (2) at = null;
    System.gc();
    System.out.println("線程代碼執行完畢,完成GC");
    printMemory();
}

private static class AsyncTask implements Runnable {
    private GCRootThread gcRootThread;

    public AsyncTask(GCRootThread gcRootThread){
        this.gcRootThread = gcRootThread;
    }

    @Override
    public void run() {
        try{
            Thread.sleep(500);
        } catch(Exception e){}
    }
}
}

打印日志:

free is 189 M, total is 192 M, 
main方法執行完畢,完成GC
free is 110 M, total is 192 M, 
線程代碼執行完畢,完成GC
free is 190 M, total is 192 M, 

分析:

  • 通過第一點我們知道虛擬機棧局部變量是可以作為GC Root的,也就是說 at 可作為GC Root,在線程還沒有結束的情況下,我在代碼(1)處將at置為null,即:將GC Root的引用鏈給斷了,但是at的成員變量(gcRootThread)并沒有被GC回收掉,說明在這個例子中at 并不是GC Root。
  • 在線程已經結束的情況下,我在代碼(1)處將at置為null,at的成員變量(gcRootThread)可以被GC回收掉,所
  • 通過上述兩點在at置為null,線程是否結束足以證明活躍線程可以作為GC Root。

結論:
程序剛開始時是 189M 內存,當調用第一次 GC 時線程并沒有執行結束,并且它作為 GC Root,所以它所引用的 80M 內存并不會被 GC 回收掉。 thread.join() 保證線程結束再調用后續代碼,所以當調用第二次 GC 時,線程已經執行完畢并被置為 null,這時線程已經被銷毀,所以之前它所引用的 80M 此時會被 GC 回收掉。

4)、成員變量可作為 GC Root

public class GCRootClassVariable {
private static int _10MB = 10 * 1024 * 1024;
private byte[] memory;
private GCRootClassVariable classVariable;

public GCRootClassVariable(int size) {
    memory = new byte[size];
}

public static void main(String[] args) {
    System.out.println("Start:");
    printMemory();
    GCRootClassVariable g = new GCRootClassVariable(4 * _10MB);
    g.classVariable = new GCRootClassVariable(8 * _10MB);
    g = null;
    System.gc();
    System.out.println("GC Finish");
    printMemory();
}
}

打印日志:

Start:
free is 189 M, total is 192 M, 
GC Finish
free is 190 M, total is 192 M,

從日志中可以看出當調用 GC 時,因為 g 已經置為 null,因此 g 中的成員變量 classVariable 此時也不再被 GC Root 引用鏈上。所以最后 g(40M) 和 classVariable(80M) 都會被回收掉。這也表明成員變量與靜態變量不同,它不會被當作 GC Root。

何時回收釋放垃圾占用的內存(何時觸發GC)

1)、Allocation Failure:在堆內存中分配時,如果因為可用剩余空間不足導致對象內存分配失敗,這時系統會觸發一次 GC。
2)、System.gc():開發者主動調用System.gc()來請求一次 GC。

如何回收垃圾

標記-清除算法(Mark and Sweep GC)

GC Roots集合開始,先掃描整個堆內存,標出所有被 GC Roots 直接或間接引用到的對象,然后執行 sweep 操作回收不可到達對象,過程分兩步:
1)、Mark 標記階段:先找到堆內存中的所有 GC Root 對象,從GC Roots集合開始,只要是和 GC Root 對象直接或者間接相連則標記為灰色,即:存活對象,否則標記為黑色,即:垃圾對象。
2)、Sweep 清除階段:當遍歷完所有的 GC Root 之后,則將標記為垃圾的對象直接清除。

如下圖所示:


mark and sweep.png

但是這種簡單的清除方式,有一個明顯的弊端,那就是內存碎片問題。

比如我申請了 1k、2k、3k、4k、5k 的內存。


mark.jpg

由于某種原因 ,2k 和 4k 的內存,我不再使用,就需要交給垃圾回收器回收。


mark2.jpg

理論上來說我應該有足足 6k 的空閑空間。那么接下來申請5k 的空間,結果系統告訴我內存不足了。系統運行時間越長,這種碎片就越多。

復制算法(Copying)

將現有內存空間分為兩塊,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象復制到未被使用的內存塊中。之后,清除正在使用的內存塊中的所有對象,交換兩個內存的角色,完成垃圾回收,即:提供一個對等的內存空間,將存活的對象復制過去,然后清除原內存空間。

1)、復制算法之前,內存分為 A/B 兩塊,并且當前只使用內存 A,內存的狀況如下圖所示:


coping_before.png

2)、標記完之后,所有可達對象都被按次序復制到內存 B 中,并設置 B 為當前使用中的內存。內存狀況如下圖所示:
coping_after.png

這種方式看似非常完美的解決了內存碎片問題。但是,它的弊端也非常明顯。它浪費了幾乎一半的內存空間來做這個事情,如果資源本來就很有限,這就是一種無法容忍的浪費。

  • 優點:按順序分配內存即可,實現簡單、運行高效,不會產生內存碎片。
  • 缺點:可用的內存大小縮小為原來的一半(所以官方引入Eden、Survivor區優化的原因),對象存活率高時會頻繁進行復制。
標記-壓縮算法

需要先從GC Root開始對所有可達對象做一次標記,然后,它并不是簡單地清理未標記的對象,這點和標志清除算法是有區別的,而是將所有的存活對象壓縮到內存的一端。最后,清理邊界外所有的空間。因此標記壓縮也分兩步完成:

1)、Mark 標記階段:找到內存中的所有 GC Root 對象,只要是和 GC Root 對象直接或者間接相連則標記為灰色(也就是存活對象),否則標記為黑色(也就是垃圾對象)。
2)、Compact 壓縮階段:將剩余存活對象按順序壓縮到內存的某一端。


mark_compat.png
  • 優點:這種方法既避免了碎片的產生,又不需要兩塊相同的內存空間,因此,其性價比比較高。
  • 缺點:所謂壓縮操作,仍需要進行局部對象移動,所以一定程度上還是降低了效率。

JVM分代回收策略

上面介紹的垃圾回收算法每個都有優缺點,沒有最好算法只有合適的算法,所以JVM引入的分代垃圾回收策略,根據不同的區域使用不同的算法。

JVM根據對象存活的周期不同,堆中的內存可以劃分為新生代(Young Generation)、老年代(Old Generation),而新生代則又可以細分為Eden 區、From Survivor 區、To Survivor 區,這就是 JVM 的內存分代策略。

JVM將堆區劃分成這么多的區域,主要是為了提高垃圾收集器(GC)對對象進行管理的效率,這樣可以根據不同的區域使用不同的垃圾回收算法,從而更具有針對性,進而提高垃圾回收效率。

注意: 在 HotSpot 中除了新生代和老年代,還有永久代。

分代回收的中心思想就是:對于新創建的對象會在新生代中分配內存,此區域的對象生命周期一般較短。如果經過多次回收仍然存活下來,則將它們轉移到老年代中。

年輕代(Young Generation)

新生成的對象會優先存放在新生代中,新生代的對象朝生夕死,存活率很低,在新生代中,常規應用進行一次垃圾收集一般可以回收 70%~95% 的空間,回收效率很高。新生代中因為要進行一些復制操作,所以一般采用的 GC 回收算法是復制算法。

年輕代細分為 3 部分:Eden、Survivor0(簡稱 S0)、Survivor1(簡稱S1)。這 3 部分按照 8:1:1 的比例來劃分新生代。這 3 塊區域的內存分配過程如下:

絕大多數剛剛被創建的對象會存放在 Eden 區,非常大的對象會直接放到老年代中。如圖所示:


young.png

當 Eden 區第一次滿的時候,會進行垃圾回收。首先將 Eden區的垃圾對象回收清除,并將存活的對象復制到 S0,此時 S1是空的。如圖所示:


young1.png

下一次 Eden 區滿時,再執行一次垃圾回收。此次會將 Eden和 S0區中所有垃圾對象清除,并將存活對象復制到 S1,此時 S0變為空。如圖所示:
young2.png

如此反復在 S0 和 S1之間切換幾次(默認 15 次)之后,如果還有存活對象。說明這些對象的生命周期較長,則將它們轉移到老年代中。如圖所示:


young3.png

需要注意的是: 每次s0和s1之間交換時,都會保證一方中沒有任何剩余對象,比如從s0轉移到s1時,如果s1的空間不足以裝下所有此時s0中的對象,則直接轉移到老年代。

年老代(Old Generation)

一個對象如果在新生代存活了足夠長的時間而沒有被GC清理掉,則會被復制到老年代,當然這只是其中一種情況會被復制到老年代。

老年代的內存大小一般比新生代大,能存放更多的對象。如果對象比較大(比如長字符串或者大數組),并且新生代的剩余空間不足,則這個大對象會直接被分配到老年代上。

我們可以使用-XX:PretenureSizeThreshold參數控制直接升入老年代的對象大小,大于這個閾值的對象會直接分配在老年代上。老年代因為對象的生命周期較長,不需要過多的復制操作,所以一般采用標記壓縮的回收算法。

老年代可能存在這么一種情況,就是老年代中的對象引用到新生代對象。這時如果要執行新生代 GC,則可能需要遍歷整個老年代上可能存在引用新生代的情況,這顯然是低效的。所以,老年代中維護了一個 512 byte 的 card table,所有老年代對象引用新生代對象的信息都記錄在這里。每當新生代發生 GC 時,只需要檢查這個 card table 即可,大大提高了性能。

GC日志分析

為了讓上層應用開發人員更加方便的調試 Java 程序,JVM 提供了相應的 GC 日志。在 GC 執行垃圾回收事件的過程中,會有各種相應的 log 被打印出來。其中新生代和老年代所打印的日志是有區別的。

  • 新生代 GC:這一區域的 GC 叫作 Minor GC。因為新生代的Java 對象大多都具備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快。
  • 老年代 GC:發生在這一區域的 GC 也叫作 Major GC 或者 Full GC。當出現了 Major GC,經常會伴隨至少一次的 Minor GC。

首先我們需要理解幾個 Java 命令的參數:

命令參數 功能描述
-verbose:gc 顯示gc的操作內容
-Xms10m 初始堆大小為 10m
-Xmx10m 設置堆的最大分配內存為10m
-Xmn10m 設置新生代的內存大小為10m
-XX:+PrintGCDetails 打印GC的詳細log日志
-XX:SurvivorRatio=8 新生代中的Eden區域和Survivor區域的帶下比值為8:1:1

下面使用代碼查看GC的日志,在內存中創建 4 個 byte 類型數組來演示內存分配與 GC 的詳細過程。代碼如下:

//VM agrs:
// -Xms20M  初始堆大小為20M
// -Xmx20M  堆最大分配內存20M
// -Xmn10M  新生代內存大小
// -XX:+PrintGCDetails
// -XX:SurvivorRatio=8 新生代比例
public class MinorGCTest {
private static final int _1MB = 1024 * 1024;

public static void testAllocation() {
      byte[] a1, a2, a3, a4;
      a1 = new byte[2 * _1MB];
      a2 = new byte[2 * _1MB];
      a3 = new byte[2 * _1MB];
      a4 = new byte[1 * _1MB];
  }
    public static void main(String[] agrs) {
        testAllocation();
    }
}

因為使用的intelLij 開發工具,我直接設置java命令參數為:-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8。通過上面的參數,可以看出堆內存總大小為 20M,其中新生代占 10M,剩下的 10M 會自動分配給老年代。執行上述代碼打印日志如下:

 Heap
 PSYoungGen   total 9216K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   eden space 8192K, 100% used [0x00000000ff600000,0x00000000ffe00000,0x00000000ffe00000)
   from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
   to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
   object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
 Metaspace       used 2670K, capacity 4486K, committed 4864K, reserved 1056768K
    class space    used 288K, capacity 386K, committed 512K, reserved 1048576K

其中日志中的各字段的含義如下:

字段 含義
PSYoungGen 新生代
eden 新生代的Eden區域
from 新生代的Survivor1區域
to 新生代的Survivor2區域
ParOldGen 老年代
Metaspace 元數據區域

從日志中可以看出:程序執行完之后,a1、a2、a3、a4 四個對象都被分配在了新生代的 Eden 區(eden space 8192K, 100% used )。

如果我們將測試代碼中的 a4 初始化改為 a4 = new byte[2 * _1MB] 則打印日志如下:

[GC (Allocation Failure) [PSYoungGen: 7128K->704K(9216K)] 7128K->6856K(19456K), 0.0033750 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 704K->0K(9216K)] [ParOldGen: 6152K->6673K(10240K)] 6856K->6673K(19456K), [Metaspace: 2664K->2664K(1056768K)], 0.0042054 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
PSYoungGen      total 9216K, used 2130K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
 eden space 8192K, 26% used [0x00000000ff600000,0x00000000ff814930,0x00000000ffe00000)
 from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen       total 10240K, used 6673K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
 object space 10240K, 65% used [0x00000000fec00000,0x00000000ff2846b0,0x00000000ff600000)
Metaspace       used 2670K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 288K, capacity 386K, committed 512K, reserved 1048576K

從日志中可以看出:分配內存是GC (Allocation Failure)失敗了的。
這是由于 在分配a4 內存之前,Eden 區已經被占用 6M(a1,a2,a3),已經無法再分配出 2M 的內存來存儲 a4 對象,所以造成的分配內存失敗,因此會執行一次 Minor GC。并嘗試將存活的 a1、a2、a3 復制到 S1 區。但是 S1 區只有 1M 空間,所以沒有辦法存儲 a1、a2、a3 任意一個對象。在這種情況下 a1、a2、a3 將被轉移到老年代,最后將 a4 保存在 Eden 區。所以最終結果就是:Eden 區占用 2M(a4),老年代占用 6M(a1、a2、a3)。當然如果Eden區也不夠存放a4,那么a4也會直接放到老年代中。

Java 四大引用

上面說過,判斷對象是否存活我們是通過GC Roots的引用可達性來判斷的。但是在Java中引用關系有四種,根據引用強度的由強到弱,分別是:強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)。

以下示例設置JVM的參數:-Xms20M 初始堆大小為20M -Xmx20M 堆最大分配內存20M

強引用

JVM中的引用默認就是強引用,任何一個對象的賦值操作就產生了對這個對象的強引用。

public class StrongReferenceTest {
private static final int _1MB = 1024 * 1024;
byte[] a1 = new byte[11 * _1MB];

public static void main(String[] args){
    //先打印內存
    printMemory();
    // 創建強引用  11M
    StrongReferenceTest strongReference = new StrongReferenceTest();
    //gc完成之后再打印內存
    printMemory();
    // 來一次 gc
    System.gc();
    //堆只有9M JVM 寧愿拋出OutOfMemoryError 運行時錯誤讓程序異常終止,也不會回收強引用所指向的對象實例
    byte[] ref1 = new byte[8 * _1MB];
}
}

輸出日志:

free is 18 M, total is 19 M, 
free is 7 M, total is 19 M, 
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.gituhub.jvm.StrongReferenceTest.main(StrongReferenceTest.java:22)

上面代碼我設置了堆大小只有20M,而我們創建了strongReference 就使用了11M,當我們嘗試創建 ref1(8M)時堆內存是明顯不夠的,這時會觸發GC,但是由于 strongReference 是強引用 不能被回收,最后JVM直接拋出OutOfMemoryError。

強引用所引用的對象實例不會被 GC 回收,JVM 寧愿拋出OutOfMemoryError 運行時錯誤讓程序異常終止,也不會回收強引用所指向的對象實例。

軟引用

軟引用是一種相對強引用弱化了一些的引用,用java.lang.ref.SoftReference實現,可以讓對象豁免一些垃圾收集。當系統內存充足的時候,不會被回收;當系統內存不足的時候,會被回收。

軟引用一般用于對內存敏感的程序中,比如高速緩存。

public class SoftReferenceTest {
public static void main(String[] args) {
    //先打印內存
    printMemory();
    // 創建強引用  11M
    RefObject object =
            new RefObject(11);
    // 因為object是強引用 為不影響 測試軟引用 把object = null


    // 使用軟引用 引用 object對象
    SoftReference<RefObject> softReference =
            new SoftReference<>(object);

    object = null;//斷開強引用


    System.out.println("softReference = " + softReference.get());
    System.out.println("object = " + object);

    //gc完成之后再打印內存
    printMemory();
    // 來一次 gc
    System.gc();
    printMemory();

    try {
        //堆只有9M JVM 寧愿拋出OutOfMemoryError 運行時錯誤讓程序異常終止,也不會回收強引用所指向的對象實例
        RefObject object8M = new RefObject(8);
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    } finally {
        System.out.println("softReference = " + softReference.get());
        System.out.println("object = " + object);
        printMemory();
    }
}

}

輸出日志:

free is 18 M, total is 19 M, 
softReference = com.gituhub.jvm.RefObject@15db9742
object = null
free is 7 M, total is 19 M, 
free is 7 M, total is 19 M, 
softReference = null
object = null
free is 10 M, total is 19 M, 

為了避免強引用的干擾我將object置為null。

我們創建了object 就使用了11M,此時內存時充足的,我們收動調用請求GC,但是softReference.get()并不為null,說明軟引用所引用的對象內存沒有被回收。
但是當我們申請 object8M時,此時系統明顯內存不足夠分配8M空間給object8M對象,這是系統主動會觸發GC,最后將軟引用所引用的對象回收,在分配8M空間給object8M對象。

可以看出在內存充足的情況下,SoftReference引用的對象是不會被回收的,相反在內存不足時,SoftReference引用的對象就會被回收掉。

弱引用

WeakReference和SoftReference很類似,不同的是WeakReference所引用的對象只要垃圾回收執行,就會被回收,而不管是否內存不足。

public class WeekReferenceTest {
public static void main(String[] args) {
    //先打印內存
    printMemory();
    // 創建強引用  11M
    RefObject object = new RefObject(11);

    WeakReference<RefObject> softReference = new WeakReference<>(object);
    // 因為object是強引用 為不影響 測試軟引用 把object = null
    object = null;//斷開強引用


    System.out.println("softReference = " + softReference.get());
    System.out.println("object = " + object);

    //gc完成之后再打印內存
    printMemory();
    // 來一次 gc
    System.gc();
    printMemory();

    System.out.println("softReference = " + softReference.get());
    System.out.println("object = " + object);

}
}

輸出日志:

free is 18 M, total is 19 M, 
softReference = com.gituhub.jvm.RefObject@15db9742
object = null
free is 7 M, total is 19 M, 
free is 18 M, total is 19 M, 
softReference = null
object = null

我們看到gc過后,弱引用的對象被回收掉了。

引用隊列ReferenceQueue

我們希望當對象被GC回收后能夠通知用戶線程,然后用戶可進行額外的處理。對于SoftReferenceWeakReference它構造函數中就有接受ReferenceQueue參數,當對象j被gc回收之后,Reference對象會被放入關聯的ReferenceQueue中。我們可以從ReferenceQueue中獲取到Reference信息,同時進行額外的處理,比如:流行的圖片處理框架Glide 就使用這種機制,Glide 使用一個HashMap管理所有正在使用的WeakReference,并且開啟一個后臺線程監控 ReferenceQueue 被WeakReference包裝的對象是否被GC回收,如果被回收了那么就需要將對應的WeakReference移除HashMap。下面的是我模擬Glide寫的一段示例代碼:

public class ReferenceQueueTest {
private static volatile boolean isShutdown = false;
private static Map<Object, ReferenceObjectWeakReference> cache = new HashMap<>();
private static ReferenceQueue<ReferenceObject> referenceQueue = new ReferenceQueue<>();

public static void main(String[] args) throws InterruptedException {
    MemoryUtil.printMemory("Main method run Start");

    ReferenceObject referenceObject10M;
    ReferenceObjectWeakReference weakReference;

    for (int i = 0; i < 3; i++) {
        referenceObject10M =
                new ReferenceObject(i + 2);//2 + 3 + 4

        int hashCode = referenceObject10M.hashCode();
        weakReference =
                new ReferenceObjectWeakReference(hashCode, referenceObject10M, referenceQueue);

        cache.put(hashCode, weakReference);
    }

    referenceObject10M = null;
    weakReference = null;


    // 開啟后臺線程監控資源回收
    Thread thread = new Thread(() -> {
        while (!isShutdown) {
            try {
                ReferenceObjectWeakReference reference = (ReferenceObjectWeakReference) referenceQueue.remove();
                cache.remove(reference.key);
                MemoryUtil.printMemory("Object is recycled and Join RefQueue");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        MemoryUtil.printLog("Monitor thread finish");
    });

    thread.setDaemon(true);
    thread.setName("GC Monitor");
    thread.start();

    MemoryUtil.printMemory("GC before");
    Thread.sleep(1000);
    System.gc();
    Thread.sleep(5000);
    isShutdown = true;
    MemoryUtil.printMemory("GC after");
    MemoryUtil.printMemory("Main method run finish : cache size:  " + cache.size());
}


static class ReferenceObjectWeakReference extends WeakReference<ReferenceObject> {
    public int key;

    public ReferenceObjectWeakReference(
            int key,
            ReferenceObject referent,
            ReferenceQueue<? super ReferenceObject> q) {
        super(referent, q);
        this.key = key;
    }
}
}

總結

這篇文章很長,主要介紹了使用可達性分析來判斷對象是否可以被回收,以及 3 種垃圾回收算法。最后通過分析 GC Log 驗證了 Java 虛擬機中內存分配及分代策略的一些細節,以及JVM中四種引用將了三種。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容