JDK源碼分析(8)之 Reference 徹底解讀

在閱讀本文以前最好對 Reference 框架有一個總體的把握,能夠參考我上一篇博客 Reference 框架概覽 ;本文主要講了 Reference 的子類實現和應用(SoftReference,WeakReference,PhantomReference);html

Java 引用的強弱關係StrongReference > SoftReference > WeakReference > PhantomReferencejava

1、StrongReference

強引用:咱們一般使用的引用,形如Object o = new Object();git

reference

此時從 stack 中的 o,到 heap 中的 Object 就是強引用;其餘引用強弱的斷定規則,能夠查看我上一篇博客 Reference 框架概覽github

2、SoftReference

軟引用:能夠用來表示一些有用但非必須的對象;JVM 會根據使用率和剩餘堆空間大小來公共決定何時回收 SoftReference;JVM 保證在拋出 OOM 以前會再次掃描回收這些軟引用,若是回收後內存仍不足纔會拋出 OOM;因此在源碼的註釋中也寫了 SoftReference 適合實現內存敏感的緩存;緩存

public class SoftReference<T> extends Reference<T> {
  /**
   * Timestamp clock, updated by the garbage collector
   */
  static private long clock;

  /**
   * Timestamp updated by each invocation of the get method.  The VM may use
   * this field when selecting soft references to be cleared, but it is not
   * required to do so.
   */
  private long timestamp;

  public SoftReference(T referent) {
    super(referent);
    this.timestamp = clock;
  }

  public SoftReference(T referent, ReferenceQueue<? super T> q) {
    super(referent, q);
    this.timestamp = clock;
  }

  public T get() {
    T o = super.get();
    if (o != null && this.timestamp != clock)
      this.timestamp = clock;
    return o;
  }

}

看上面的代碼,SoftReference 與 Reference 相比多了兩個時間戳 clock,timestamp,而且會在每次 get的時候更新時間戳;app

  • clock:這個時間戳是static修飾的,是全部 SoftReference 共有,由 JVM 維護;
  • timestamp:主要用於記錄當前對象的存活時間;

回收策略框架

上面提到 SoftReference 的回收是由使用率和剩餘堆空間大小來公共決定的,那麼它是怎麼實現的呢?ide

openjdk/hotspot/src/share/vm/memory/referencePolicy.cppoop

// Capture state (of-the-VM) information needed to evaluate the policy
void LRUCurrentHeapPolicy::setup() {
  _max_interval = (Universe::get_heap_free_at_last_gc() / M) * SoftRefLRUPolicyMSPerMB;
  assert(_max_interval >= 0,"Sanity check");
}

// The oop passed in is the SoftReference object, and not
// the object the SoftReference points to.
bool LRUCurrentHeapPolicy::should_clear_reference(oop p, jlong timestamp_clock) {
  jlong interval = timestamp_clock - java_lang_ref_SoftReference::timestamp(p);
  assert(interval >= 0, "Sanity check");

  // The interval will be zero if the ref was accessed since the last scavenge/gc.
  if(interval <= _max_interval) {
    return false;
  }

  return true;
}

根據上面的代碼能夠大體知道:post

  1. 首先計算出了最大堆內存和上次 GC 時剩餘的內存;
  2. 再用(剩餘內存 / 最大內存 )* SoftRefLRUPolicyMSPerMB 得出到下次 GC 期間軟引用的最大 idle 時間;
  3. 最後用 clock 和 timestamp 兩個時間戳差值獲得 SoftReference 的 idle 時間(每次 get 的時候 this.timestamp = clock;,因此get 以後 idle 時間歸零),若是大於最大 idle 時間則清除;

咱們能夠簡單測試一下,啓動參數:-XX:SoftRefLRUPolicyMSPerMB=2 -Xmx10M -XX:+PrintCommandLineFlags -verbose:gc

  • -XX:SoftRefLRUPolicyMSPerMB=2:能夠參照上面的計算過程調節 SoftReference 的回收頻率;
  • -Xmx10M:爲最大堆內存,一樣能夠自行調節,-verbose:gc:打開 GC 日誌,-XX:+PrintCommandLineFlags:打印 JVM 啓動參數;
private static void test03() throws InterruptedException {
  ReferenceQueue queue = new ReferenceQueue();
  Object o = new Object() {
    @Override
    public String toString() {
      return "zhangsan";
    }
  };
  
  Reference softRef = new SoftReference(o, queue);
  new Monitor(queue).start();
  
  o = null;
  System.gc();
  log.info("o=null, referent:{}", softRef.get());
  
  byte[] bytes = new byte[3 * 1024 * 1024];
  System.gc();
  log.info("After GC, referent:{}", softRef.get());
  Thread.sleep(2000);
  System.gc();
  log.info("After GC, referent:{}", softRef.get());
}

private static class Monitor extends Thread {
  ReferenceQueue queue;

  public Monitor(ReferenceQueue queue) {
    this.queue = queue;
  }
  
  @Override
  public void run() {
    while (true) {
      try {
        log.info("remove reference:{}", queue.remove().toString());
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
}

// 打印:

[main] o=null, referent:zhangsan
[main] After GC, referent:zhangsan
[main] After GC, referent:null
[Thread-0] remove reference:java.lang.ref.SoftReference@bcffe9a

根據不一樣的參數設置會出現不一樣的狀況,你們能夠自行調節參數,驗證上面的計算規則;另外若是-XX:SoftRefLRUPolicyMSPerMB=0,那麼 SoftReference 就應該和 WeakReference 差很少了,至因而否徹底一致,就留到之後查看 JVM 的時候再肯定了;

3、WeakReference

弱引用:被弱引用關聯的對象只能生存到下一次 GC,當 GC 的時候不管內存是否足夠,使用是否頻繁都會被清除;一樣源碼註釋裏面也寫了 WeakReference 適合實現 canonicalizing mappings,好比 WeakHashMap;

public class WeakReference<T> extends Reference<T> {
  public WeakReference(T referent) {
    super(referent);
  }

  public WeakReference(T referent, ReferenceQueue<? super T> q) {
    super(referent, q);
  }
}

簡單測試,啓動參數:-Xmx300M -XX:+PrintCommandLineFlags -verbose:gc

private static void test04() {
  ReferenceQueue queue = new ReferenceQueue();
  Object o = new Object() {
    @Override
    public String toString() {
      return "zhangsan";
    }
  };
  
  Reference ref = new WeakReference(o, queue);
  new Monitor(queue).start();
  
  o = null;
  log.info("Before GC, referent:{}", ref.get());
  System.gc();
  log.info("After GC, referent:{}", ref.get());
}

// 打印:

[main]     Before GC, referent:zhangsan
[main]     After GC, referent:null
[Thread-0] remove reference:java.lang.ref.WeakReference@67ac4ff0

能夠看到在內存足夠的時候,referent 被清除,WeakReference 在下次 GC 的時候隨機被清除,而且 ReferenceQueue 也收到了事件通知;

4、PhantomReference

虛引用:最弱的一種引用關係,虛引用對一個對象的生命週期徹底沒有影響,設置虛引用的惟一目的就是獲得 referent 被回收的事件通知;

public class PhantomReference<T> extends Reference<T> {
    public T get() {
        return null;
    }

    public PhantomReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

從源碼也能看到 get 的時候,永遠返回 null;

一樣簡單測試一下,

private static void test06() {
  ReferenceQueue queue = new ReferenceQueue();
  Object o = new Object() {
    @Override
    public String toString() {
      return "zhangsan";
    }
  };
  
  Reference ref = new PhantomReference(o, queue);
  new Monitor(queue).start();
  
  o = null;
  log.info("Before GC, referent:{}", ref.get());
  System.gc();
  log.info("After GC, referent:{}", ref.get());
}

// 打印:

[main]     Before GC, referent:null
[main]     After GC, referent:null
[Thread-0] remove reference:java.lang.ref.PhantomReference@661a5fff

能夠看到 PhantomReference.get() 始終爲 null,而且當 referent 被回收的時候,而且 ReferenceQueue 也收到了事件通知;

此外 PhantomReference 和其餘引用還有一個很大的不一樣,在 ReferenceQueue 中 JVM 並不會幫咱們把 referent 字段置爲空;

private static void test07() {
  ReferenceQueue queue = new ReferenceQueue();
  Object o = new Object() {
    @Override
    public String toString() {
      return "zhangsan";
    }
  };
  
  Reference ref = new PhantomReference(o, queue);
  new Monitor2(queue).start();
  
  o = null;
  log.info("Before GC, referent:{}", ref.get());
  System.gc();
  log.info("After GC, referent:{}", ref.get());
}

private static class Monitor2 extends Thread {
  ReferenceQueue queue;
  
  public Monitor2(ReferenceQueue queue) {
    this.queue = queue;
  }
  
  @Override
  public void run() {
    try {
      while (true) {
        Reference ref = queue.poll();
        log.info("remove reference:{}", ref);
        if (ref != null) {
        Field field = Reference.class.getDeclaredField("referent");
        field.setAccessible(true);
        
        log.info("ReferenceQueue get Referent:{}", field.get(ref));
        ref.clear();
        break;
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

// 打印:

[main]     Before GC, referent:null
[main]     After GC, referent:null
[Thread-0] remove reference:null
[Thread-0] remove reference:java.lang.ref.PhantomReference@7b4cba2
[Thread-0] ReferenceQueue get Referent:zhangsan

這裏能夠看到從 ReferenceQueue 中取出來的 Reference 仍然能夠取到引用對象,即 referent;可是在其餘引用中打印爲 null,這裏能夠將上面例子中的 Monitor 改成 Monitor2 測試;

Cleaner
Reference.tryHandlePending()裏面提到的,主要用於替代Object.finalize();

public class Cleaner extends PhantomReference<Object> {
  private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();
  static private Cleaner first = null;

  private Cleaner
    next = null,
    prev = null;
    
  private final Runnable thunk;
  
  private Cleaner(Object referent, Runnable thunk) {
    super(referent, dummyQueue);
    this.thunk = thunk;
  }
  
  public static Cleaner create(Object ob, Runnable thunk) {
    if (thunk == null)
      return null;
    return add(new Cleaner(ob, thunk));
  }

  private static synchronized Cleaner add(Cleaner cl) {
    if (first != null) {
      cl.next = first;
      first.prev = cl;
    }
    first = cl;
    return cl;
  }

  private static synchronized boolean remove(Cleaner cl) { }

  public void clean() {
    if (!remove(this))
      return;
    try {
      thunk.run();
    } catch (final Throwable x) {
      AccessController.doPrivileged(new PrivilegedAction<Void>() {
          public Void run() {
            if (System.err != null)
              new Error("Cleaner terminated abnormally", x)
                .printStackTrace();
            System.exit(1);
            return null;
          }});
    }
  }
}

從代碼能夠看到,

  • Cleaner 只能經過工廠方法建立,而且全部的 Cleaner 都共同屬於同一個 Reference 鏈表;
  • 代碼中的next、prev不一樣於 Reference 中的 next,他們組成了一個雙向鏈表;
  • Cleaner 中沒有入隊操做,在建立之初就已經加入鏈表了,具體代碼能夠查看Reference.tryHandlePending()
  • ReferenceQueue(dummyQueue 域)的做用再也不是提供入隊和事件監聽功能,而僅僅是保證 GC 不會自動將 Cleaner 給回收了;
  • Cleaner 的主要邏輯就是傳入一個 clean 線程,在 referent 引用對象清除的時候,執行 clean 操做;

總結

  • 對於上面講的軟引用、弱引用、虛引用,都有一套共同的事件通知機制,具體邏輯在 Reference 類中;主要的差異在於引用回收條件的判斷,這部分代碼在 JVM 裏面;
  • 另外對於 Reference 類還有 FinalReference 沒有寫,主要用於當類重寫finalize方法時,JVM 會將他包裝在 FinalReference 裏面,裏面的細節比較多,而且通常不建議使用,因此暫時沒寫;
  • 此外《Effective Java》第三版的第八條也講了避免使用finalizer和cleaner;詳情能夠自行查閱;

參考

http://www.importnew.com/21628.html
https://www.jianshu.com/p/95a4931ebf01
http://www.javashuo.com/article/p-sqolikpq-kn.html

相關文章
相關標籤/搜索