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
的時候更新時間戳;
- clock:這個時間戳是
static
修飾的,是所有 SoftReference 共有,由 JVM 維護; - timestamp:主要用於記錄當前對象的存活時間;
回收策略
上面提到 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; }
根據上面的代碼可以大致知道:
- 首先計算出了最大堆內存和上次 GC 時剩余的內存;
- 再用(剩余內存 / 最大內存 )* SoftRefLRUPolicyMSPerMB 得出到下次 GC 期間軟引用的最大 idle 時間;
- 最後用 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 的時候再確定了;
三、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;
}});
}
}
}
從代碼可以看到,
- Cleaner 只能通過工廠方法創建,並且所有的 Cleaner 都共同屬於同一個 Reference 鏈表;
- 代碼中的
next、prev
不同於 Reference 中的 next,他們組成了一個雙向鏈表; - Cleaner 中沒有入隊操作,在創建之初就已經加入鏈表了,具體代碼可以查看
Reference.tryHandlePending()
; - Cleaner 的主要邏輯就是傳入一個clean線程,在 referent 引用對象清除的時候,這行這個清楚操作;
總結
- 對於上面講的軟引用、弱引用、虛引用,都有一套共同的事件通知機制,具體邏輯在 Reference 類中;主要的差別在於引用回收條件的判斷,這部分代碼在 JVM 裏面;
- 另外對於 Reference 類還有 FinalReference 沒有寫,主要用於當類重寫
finalize
方法時,JVM 會將他包裝在 FinalReference 裏面,裏面的細節比較多,並且一般不建議使用,所以暫時沒寫; - 此外《Effective Java》第三版的第八條也講了避免使用finalizer和cleaner;詳情可以自行查閱;
參考
http://www.importnew.com/21628.html
https://www.jianshu.com/p/95a4931ebf01
https://juejin.im/post/5bbfee46e51d450e5e0cba2f
JDK源碼分析(8)之 Reference 實現和應用