1. 程式人生 > 程式設計 >深入瞭解JAVA 軟引用

深入瞭解JAVA 軟引用

定義

軟引用是使用SoftReference建立的引用,強度弱於強引用,被其引用的物件在記憶體不足的時候會被回收,不會產生記憶體溢位。

說明

軟引用,顧名思義就是比較“軟”一點的引用。

當一個物件與GC Roots之間存在強引用時,無論何時都不會被GC回收掉。如果一個物件與GC Roots之間沒有強引用與其關聯而存在軟引用關聯時,那麼垃圾回收器對它的態度就取決於記憶體的緊張程度了。如果記憶體空間足夠,垃圾回收器就不會回收這個物件,但如果記憶體空間不足了,它就難逃被回收的厄運。

如果一個物件與GC Roots之間不存在強引用,但是存在軟引用,則稱這個物件為軟可達(soft reachable)物件。

在垃圾回收器沒有回收它的時候,軟可達物件就像強可達物件一樣,可以被程式正常訪問和使用,但是需要通過軟引用物件間接訪問,需要的話也能重新使用強引用將其關聯。所以軟引用適合用來做記憶體敏感的快取記憶體。

String s = new String("Frank");  // 建立強引用與String物件關聯,現在該String物件為強可達狀態
SoftReference<String> softRef = new SoftReference<String>(s);   // 再建立一個軟引用關聯該物件
s = null;    // 消除強引用,現在只剩下軟引用與其關聯,該String物件為軟可達狀態
s = softRef.get(); // 重新關聯上強引用

這裡變數s持有對字串物件的強引用,而softRef持有對該物件的軟引用,所以當執行s = null後,字串物件就只剩下軟引用了,這時如果因為記憶體不足發生Full GC,就會把這個字串物件回收掉。

注意,在垃圾回收器回收一個物件前,SoftReference類所提供的get方法會返回Java物件的強引用,一旦垃圾執行緒回收該物件之後,get方法將返回null。所以在獲取軟引用物件的程式碼中,一定要先判斷返回是否為null,以免出現NullPointerException異常而導致應用崩潰。

下面的程式碼會讓s再次持有物件的強引用:

s = softRef.get();

如果在softRef指向的物件被回收前,用強引用指向該物件,那這個物件又會變成強可達。

來看一個使用SoftReference的栗子:

public class TestA {
  static class OOMClass{
    private int[] oom = new int[1024 * 100];// 100KB
  }

  public static void main(String[] args) throws InterruptedException {
    ReferenceQueue<OOMClass> queue = new ReferenceQueue<>();
    List<SoftReference> list = new ArrayList<>();
    while(true){
      for (int i = 0; i < 100; i++) {
        list.add(new SoftReference<OOMClass>(new OOMClass(),queue));
      }
      Thread.sleep(500);
    }
  }
}

注意,ReferenceQueue中宣告的型別為OOMClass,即與SoftReference引用的型別一致。

設定一下虛擬機器引數:

-verbose:gc -Xms4m -Xmx4m -Xmn2m

執行結果:

[GC (Allocation Failure) 1017K->432K(3584K),0.0017239 secs]
[GC (Allocation Failure) 1072K->472K(3584K),0.0099237 secs]
[GC (Allocation Failure) 1323K->1296K(3584K),0.0009528 secs]
[GC (Allocation Failure) 2114K->2136K(3584K),0.0009951 secs]
[Full GC (Ergonomics) 2136K->1992K(3584K),0.0040658 secs]
[Full GC (Ergonomics) 2807K->2791K(3584K),0.0036280 secs]
[Full GC (Allocation Failure) 2791K->373K(3584K),0.0032477 secs]
[Full GC (Ergonomics) 2786K->2773K(3584K),0.0034554 secs]
[Full GC (Allocation Failure) 2773K->373K(3584K),0.0032667 secs]
[Full GC (Ergonomics) 2798K->2775K(3584K),0.0036231 secs]
[Full GC (Allocation Failure) 2775K->375K(3584K),0.0055482 secs]
[Full GC (Ergonomics) 2799K->2776K(3584K),0.0031358 secs]
...省略n次GC資訊

在TestA中,我們使用死迴圈不斷的往list中新增新物件,如果是強引用,會很快因為記憶體不足而丟擲OOM,因為這裡的堆記憶體大小設定為了4M,而一個物件就有100KB,一個迴圈新增100個物件,也就是差不多10M,顯然一個迴圈都跑不完就會記憶體不足,而這裡,因為使用的是軟引用,所以JVM會在記憶體不足的時候將軟引用回收掉。

[Full GC (Allocation Failure) 2791K->373K(3584K),0.0032477 secs]

從這一條可以看出,在記憶體不足發生Full GC時,回收掉了大部分的軟引用指向的物件,釋放了大量的記憶體。

因為這裡新生代只分配了2M,所以很快就會發生GC,如果你的程式執行沒有看到這個結果,請先確認一下虛擬機器引數是否設定正確,如果設定正確還是沒有看到,那麼將迴圈次數由1000改為10000或者100000在試試看。

應用場景

軟引用關聯的物件,只有在記憶體不足的時候JVM才會回收該物件。這一點可以很好地用來解決OOM的問題,並且這個特性很適合用來實現快取:比如網頁快取、圖片快取等。

現在考慮這樣一個場景 ,在很多應用中,都會出現大量的預設圖片,比如說QQ的預設頭像,應用內的預設圖示等等,這些圖片很多地方會用到。

如果每次都去讀取圖片,由於讀取檔案速度較慢,大量重複的讀取會導致效能下降。所以可以考慮將圖片快取起來,需要的時候直接從記憶體中讀取。但是,由於圖片佔用記憶體空間比較大,快取的圖片過多會佔用比較多的記憶體,就可能比較容易發生OOM。這時候,軟引用就派得上用場了。

注意,SoftReference物件是用來儲存軟引用的,但它同時也是一個Java物件。所以,當軟可及物件被回收之後,雖然這個SoftReference物件的get()方法返回null,但SoftReference物件本身並不是null,而此時這個SoftReference物件已經不再具有存在的價值,需要一個適當的清除機制,避免大量SoftReference物件帶來的記憶體洩漏。

ReferenceQueue就是用來儲存這些需要被清理的引用物件的。軟引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果軟引用所引用的物件被垃圾回收器回收,Java虛擬機器就會把這個軟引用加入到與之關聯的引用佇列中。

下面用SoftReference來實現一個簡單的快取類:

public class SoftCache<T> {
  // 引用佇列
  private ReferenceQueue<T> referenceQueue = new ReferenceQueue<>();
  // 儲存軟引用集合,在引用物件被回收後銷燬
  private List<Reference<T>> list = new ArrayList<>();

  // 新增快取物件
  public synchronized void add(T obj){
    // 構建軟引用
    Reference<T> reference = new SoftReference<T>(obj,referenceQueue);
    // 加入列表中
    list.add(reference);
  }

  // 獲取快取物件
  public synchronized T get(int index){
    // 先對無效引用進行清理
    clear();
    if (index < 0 || list.size() < index){
      return null;
    }
    Reference<T> reference = list.get(index);
    return reference == null ? null : reference.get();
  }

  public int size(){
    return list.size();
  }

  @SuppressWarnings("unchecked")
  private void clear(){
    Reference<T> reference;
    while (null != (reference = (Reference<T>) referenceQueue.poll())){
      list.remove(reference);
    }
  }
}

然後測試一下這個快取類:

public class SoftCacheTest {
  private static int num = 0;

  public static void main(String[] args){
    SoftCache<OOMClass> softCache = new SoftCache<>();
    for (int i = 0; i < 40; i++) {
      softCache.add(new OOMClass("OOM Obj-" + ++num));
    }
    System.out.println(softCache.size());
    for (int i = 0; i < softCache.size(); i++) {
      OOMClass obj = softCache.get(i);
      System.out.println(obj == null ? "null" : obj.name);
    }
    System.out.println(softCache.size());
  }

  static class OOMClass{
    private String name;
    private int[] oom = new int[1024 * 100];// 100KB

    public OOMClass(String name) {
      this.name = name;
    }
  }
}

仍使用之前的虛擬機器引數:

-verbose:gc -Xms4m -Xmx4m -Xmn2m

執行結果:

[GC (Allocation Failure) 1017K->432K(3584K),0.0012236 secs]
[GC (Allocation Failure) 1117K->496K(3584K),0.0016875 secs]
[GC (Allocation Failure) 1347K->1229K(3584K),0.0015059 secs]
[GC (Allocation Failure) 2047K->2125K(3584K),0.0018090 secs]
[Full GC (Ergonomics) 2125K->1994K(3584K),0.0054759 secs]
[Full GC (Ergonomics) 2822K->2794K(3584K),0.0023167 secs]
[Full GC (Allocation Failure) 2794K->376K(3584K),0.0036056 secs]
[Full GC (Ergonomics) 2795K->2776K(3584K),0.0042365 secs]
[Full GC (Allocation Failure) 2776K->376K(3584K),0.0035122 secs]
[Full GC (Ergonomics) 2795K->2776K(3584K),0.0054760 secs]
[Full GC (Allocation Failure) 2776K->376K(3584K),0.0036965 secs]
[Full GC (Ergonomics) 2802K->2777K(3584K),0.0044513 secs]
[Full GC (Allocation Failure) 2777K->376K(3584K),0.0041400 secs]
[Full GC (Ergonomics) 2796K->2777K(3584K),0.0025255 secs]
[Full GC (Allocation Failure) 2777K->376K(3584K),0.0037690 secs]
[Full GC (Ergonomics) 2817K->2777K(3584K),0.0037759 secs]
[Full GC (Allocation Failure) 2777K->377K(3584K),0.0042416 secs]
快取列表大小:40
OOM Obj-37
OOM Obj-38
OOM Obj-39
OOM Obj-40
快取列表大小:4

可以看到,快取40個軟引用物件之後,如果一次性全部儲存,顯然記憶體大小無法滿足,所以在不斷建立軟引用物件的過程中,不斷髮生GC來進行垃圾回收,最終只有4個軟引用未被清理掉。

強引用與軟引用對比

沒有對比就沒有傷害,來將強引用和軟引用對比一下:

public class Test {

  static class OOMClass{
    private int[] oom = new int[1024];
  }

  public static void main(String[] args) {
    testStrongReference();
    //testSoftReference();
  }

  public static void testStrongReference(){
    List<OOMClass> list = new ArrayList<>();
    for (int i = 0; i < 1000; i++) {
      list.add(new OOMClass());
    }
  }

  public static void testSoftReference(){
    ReferenceQueue<OOMClass> referenceQueue = new ReferenceQueue<>();
    List<SoftReference> list = new ArrayList<>();
    for (int i = 0; i < 1000; i++) {
      OOMClass oomClass = new OOMClass();
      list.add(new SoftReference(oomClass,referenceQueue));
      oomClass = null;
    }
  }
}

執行testStrongReference方法的結果如下:

[GC (Allocation Failure) 1019K->384K(3584K),0.0033595 secs]
[GC (Allocation Failure) 1406K->856K(3584K),0.0013098 secs]
[GC (Allocation Failure) 1880K->1836K(3584K),0.0014382 secs]
[Full GC (Ergonomics) 1836K->1756K(3584K),0.0039761 secs]
[Full GC (Ergonomics) 2778K->2758K(3584K),0.0021269 secs]
[Full GC (Ergonomics) 2779K->2770K(3584K),0.0016329 secs]
[Full GC (Ergonomics) 2779K->2775K(3584K),0.0023157 secs]
[Full GC (Ergonomics) 2775K->2775K(3584K),0.0015927 secs]
[Full GC (Ergonomics) 3037K->3029K(3584K),0.0025071 secs]
[Full GC (Ergonomics) 3067K->3065K(3584K),0.0017529 secs]
[Full GC (Allocation Failure) 3065K->3047K(3584K),0.0033445 secs]
[Full GC (Ergonomics) 3068K->3059K(3584K),0.0016623 secs]
[Full GC (Ergonomics) 3070K->3068K(3584K),0.0028357 secs]
[Full GC (Allocation Failure) 3068K->3068K(3584K),0.0017616 secs]
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid3352.hprof ...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
Heap dump file created [3855956 bytes in 0.017 secs]
[Full GC (Ergonomics) 3071K->376K(3584K),0.0032068 secs]
at reference.Test$OOMClass.<init>(Test.java:11)
at reference.Test.testStrongReference(Test.java:22)
at reference.Test.main(Test.java:15)

Process finished with exit code 1

可以看到,很快就丟擲了OOM,原因是Java heap space,也就是堆記憶體不足。

如果執行testSoftReference方法,將會得到如下結果:

[GC (Allocation Failure) 1019K->464K(3584K),0.0019850 secs]
[GC (Allocation Failure) 1484K->844K(3584K),0.0015920 secs]
[GC (Allocation Failure) 1868K->1860K(3584K),0.0043236 secs]
[Full GC (Ergonomics) 1860K->1781K(3584K),0.0044581 secs]
[Full GC (Ergonomics) 2802K->2754K(3584K),0.0041726 secs]
[Full GC (Ergonomics) 2802K->2799K(3584K),0.0031293 secs]
[Full GC (Ergonomics) 3023K->3023K(3584K),0.0024830 secs]
[Full GC (Ergonomics) 3071K->3068K(3584K),0.0035025 secs]
[Full GC (Allocation Failure) 3068K->405K(3584K),0.0040672 secs]
[GC (Allocation Failure) 1512K->1567K(3584K),0.0011170 secs]
[Full GC (Ergonomics) 1567K->1496K(3584K),0.0048438 secs]

可以看到,並沒有丟擲OOM,而是進行多次了GC,可以明顯的看到這一條:

[Full GC (Allocation Failure) 3068K->405K(3584K),0.0040672 secs]

當記憶體不足時進行了一次Full GC,回收了大部分記憶體空間,也就是將大部分軟引用指向的物件回收掉了。

小結

  • 軟引用弱於強引用
  • 軟引用指向的物件會在記憶體不足時被垃圾回收清理掉
  • JVM會優先回收長時間閒置不用的軟引用物件,對那些剛剛構建的或剛剛使用過的軟引用物件會盡可能保留
  • 軟引用可以有效的解決OOM問題
  • 軟引用適合用作非必須大物件的快取

至此,本篇就告一段落了,這裡只簡單的介紹了軟引用的作用以及用法。其實軟引用並沒有這麼好,它的使用有一些可能是致命的缺點,如果想要更深入的瞭解軟引用的執行原理以及軟引用到底是在何時進行回收,又是如何進行回收的話,可以檢視翻閱後續的章節。

以上就是深入瞭解JAVA 軟引用的詳細內容,更多關於JAVA 軟引用的資料請關注我們其它相關文章!