JDK原始碼分析(8)之 Reference 實現和應用
在閱讀本文之前最好對 Reference 框架有一個整體的把握,可以參考我上一篇部落格Reference 框架概覽 ;本文主要講了 Reference
的子類實現和應用( SoftReference,WeakReference,PhantomReference
);
Java 引用的強弱關係: StrongReference > SoftReference > WeakReference > PhantomReference
一、StrongReference
強引用:我們通常使用的引用,形如 Object o = new Object();
此時從 stack 中的 o,到 heap 中的 Object 就是強引用;其他引用強弱的判定規則,可以檢視我上一篇部落格Reference 框架概覽 ;
二、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
的時候更新時間戳;
static
回收策略
上面提到 SoftReference 的回收是由使用率和剩餘堆空間大小來公共決定的,那麼它是怎麼實現的呢?
openjdk/hotspot/src/share/vm/memory/referencePolicy.cpp
// 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; }
根據上面的程式碼可以大致知道:
this.timestamp = clock;
我們可以簡單測試一下,啟動引數: -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 的時候再確定了;
三、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 也收到了事件通知;
四、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; }}); } } }
從程式碼可以看到,
next、prev Reference.tryHandlePending()
總結
finalize