1. 程式人生 > 程式設計 >詳解 JAVA 弱引用

詳解 JAVA 弱引用

定義

弱引用是使用WeakReference建立的引用,弱引用也是用來描述非必需物件的,它是比軟引用更弱的引用型別。在發生GC時,只要發現弱引用,不管系統堆空間是否足夠,都會將物件進行回收。

說明

弱引用,從名字來看就很弱嘛,這種引用指向的物件,一旦在GC時被掃描到,就逃脫不了被回收的命運。

但是,弱引用指向的物件也並不一定就馬上會被回收,如果弱引用物件較大,直接進到了老年代,那麼就可以苟且偷生到Full GC觸發前,所以弱引用物件也可能存在較長的一段時間。一旦一個弱引用物件被垃圾回收器回收,便會加入到一個引用佇列中(如果有的話)。

弱引用對應的類為WeakReference,舉個栗子:

String s = new String("Frank");  
WeakReference<String> weakRef = new WeakReference<String>(s);
s = null;

這裡我們把s設定為null後,字串物件便只有弱引用指向它。

弱可達

如果一個物件與GC Roots之間僅存在弱引用,則稱這個物件為弱可達(weakly reachable)物件。

注意

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

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

s = weakRef.get();

如果在weakRef包裹的物件被回收前,用強引用關聯該物件,那這個物件又會變成強可達狀態。

來看一個簡單的栗子瞭解一下WeakReference引用的物件是何時被回收的:

public class WeakReferenceTest {
  private static final List<Object> TEST_DATA = new LinkedList<>();
  private static final ReferenceQueue<TestClass> QUEUE = new ReferenceQueue<>();

  public static void main(String[] args) {
    TestClass obj = new TestClass("Test");
    WeakReference<TestClass> weakRef = new WeakReference<>(obj,QUEUE);
    //可以重新獲得OOMClass物件,並用一個強引用指向它
    //oomObj = weakRef.get();

    // 該執行緒不斷讀取這個弱引用,並不斷往列表裡插入資料,以促使系統早點進行GC
    new Thread(() -> {
      while (true) {
        TEST_DATA.add(new byte[1024 * 100]);
        try {
          Thread.sleep(1000);
        } catch (InterruptedException e) {
          e.printStackTrace();
          Thread.currentThread().interrupt();
        }
        System.out.println(weakRef.get());
      }
    }).start();

    // 這個執行緒不斷讀取引用佇列,當弱引用指向的物件唄回收時,該引用就會被加入到引用佇列中
    new Thread(() -> {
      while (true) {
        Reference<? extends TestClass> poll = QUEUE.poll();
        if (poll != null) {
          System.out.println("--- 弱引用物件被jvm回收了 ---- " + poll);
          System.out.println("--- 回收物件 ---- " + poll.get());
        }
      }
    }).start();

    //將強引用指向空指標 那麼此時只有一個弱引用指向TestClass物件
    obj = null;

    try {
      Thread.currentThread().join();
    } catch (InterruptedException e) {
      e.printStackTrace();
      System.exit(1);
    }
  }

  static class TestClass {
    private String name;

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

    @Override
    public String toString() {
      return "TestClass - " + name;
    }
  }
}

設定一下虛擬機器引數:

-verbose:gc -Xms4m -Xmx4m -Xmn2m

執行結果如下:

[GC (Allocation Failure) 1017K->464K(3584K),0.0014345 secs]
[GC (Allocation Failure) 1483K->536K(3584K),0.0017221 secs]
[GC (Allocation Failure) 1560K->648K(3584K),0.0036572 secs]
TestClass - Test
TestClass - Test
TestClass - Test
[GC (Allocation Failure) 1621K->984K(3584K),0.0011455 secs]
--- 弱引用物件被jvm回收了 ---- java.lang.ref.WeakReference@51a947fe
--- 回收物件 ---- null
null
...省略n個null和幾次GC資訊
[Full GC (Ergonomics) 2964K->2964K(3584K),0.0025450 secs]
[Full GC (Allocation Failure) 2964K->2964K(3584K),0.0021907 secs]
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid6860.hprof ...
Heap dump file created [3912229 bytes in 0.011 secs]
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
at weakhashmap.WeakReferenceTest.lambda$main$0(WeakReferenceTest.java:22)
at weakhashmap.WeakReferenceTest$$Lambda$1/764977973.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)

可以看到,其實弱引用也並不是一發生GC就被回收掉了。

應用場景

如果一個物件僅僅是偶爾使用,並且希望在使用時隨時就能獲取到,但又不想影響此物件的垃圾收集,那麼你應該用 WeakReference 來引用該物件。

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

一般來說,很少直接使用WeakReference,而是使用WeakHashMap。在WeakHashMap中,內部有一個引用佇列,插入的元素會被包裹成WeakReference,並加入佇列中,用來做快取再合適不過。

在Tomcat的快取中,其實就用到了WeakHashMap:

public final class ConcurrentCache<K,V> {
  private final int size;
  private final Map<K,V> eden;
  private final Map<K,V> longterm;

  public ConcurrentCache(int size) {
    this.size = size;
    this.eden = new ConcurrentHashMap<>(size);
    this.longterm = new WeakHashMap<>(size);
  }

  public V get(K k) {
    // 先從eden中取
    V v = this.eden.get(k);
    if (v == null) {
      // 如果取不到再從longterm中取
      synchronized (longterm) {
        v = this.longterm.get(k);
      }
      // 如果取到則重新放到eden中
      if (v != null) {
        this.eden.put(k,v);
      }
    }
    return v;
  }

  public void put(K k,V v) {
    if (this.eden.size() >= size) {
      // 如果eden中的元素數量大於指定容量,將所有元素放到longterm中
      synchronized (longterm) {
        this.longterm.putAll(this.eden);
      }
      this.eden.clear();
    }
    this.eden.put(k,v);
  }
}

這裡有eden和longterm的兩個map,如果對jvm堆了解的話,可以看出tomcat在這裡是使用ConcurrentHashMap和WeakHashMap做了類似分代快取的操作。

在put方法裡,在插入鍵值對時,先檢查eden快取的容量是否超出設定的大小。如果沒有則直接放入eden快取,如果超了則鎖定longterm將eden中所有的鍵值對都放入longterm。再將eden清空並插入該鍵值對。

在get方法中,也是優先從eden中找對應的key,如果沒有則進入longterm快取中查詢,找到後就加入eden快取並返回。

經過這樣的設計,相對常用的物件都能在eden快取中找到,不常用(有可能被銷燬的物件)的則進入longterm快取。而longterm的key的實際物件沒有其他引用指向它時,gc就會自動回收heap中該弱引用指向的實際物件,並將弱引用放入其引用佇列中。

弱引用與軟引用對比

弱引用與軟引用的區別在於:

  1. 只具有弱引用的物件擁有更短暫的生命週期。
  2. 被垃圾回收器回收的時機不一樣,在垃圾回收器執行緒掃描它所管轄的記憶體區域的過程中,一旦發現了只具有弱引用的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體。而被軟引用關聯的物件只有在記憶體不足時才會被回收。
  3. 弱引用不會影響GC,而軟引用會一定程度上對GC造成影響。

相似之處:都是用來描述非必需物件的。

那麼什麼時候用SoftReference,什麼時候用WeakReference呢?

如果快取的物件是比較大的物件,使用頻率相對較高的物件,那麼使用SoftReference會更好,因為這樣能讓快取物件有更長的生命週期。

如果快取物件都是比較小的物件,使用頻率一般或者相對較低,那麼使用WeakReference會更合適。

當然,如果實在不知道選哪個,一般而言,用作快取時使用WeakHashMap都不會有太大問題。

小結

  • 弱引用是比軟引用更弱的引用型別
  • 弱引用不能延長物件的生命週期,一旦物件只剩下弱引用,它就隨時可能會被回收
  • 可以通過弱引用獲取物件的強引用
  • 弱引用適合用作快取

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