Java引用型別原理剖析
Java中一共有4種引用型別(其實還有一些其他的引用型別比如FinalReference):強引用、軟引用、弱引用、虛引用。其中強引用就是我們經常使用的Object a = new Object();
這樣的形式,在Java中並沒有對應的Reference類。
本篇文章主要是分析軟引用、弱引用、虛引用的實現,這三種引用型別都是繼承於Reference這個類,主要邏輯也在Reference中。
更多文章見個人部落格:github.com/farmerjohng…
問題
在分析前,先拋幾個問題?
1.網上大多數文章對於軟引用的介紹是:在記憶體不足的時候才會被回收,那記憶體不足是怎麼定義的?什麼才叫記憶體不足?
2.網上大多數文章對於虛引用的介紹是:形同虛設,虛引用並不會決定物件的生命週期。主要用來跟蹤物件被垃圾回收器回收的活動。真的是這樣嗎?
3.虛引用在Jdk中有哪些場景下用到了呢?
Reference
我們先看下Reference.java
中的幾個欄位
public abstract class Reference<T> {
//引用的物件
private T referent;
//回收佇列,由使用者在Reference的建構函式中指定
volatile ReferenceQueue<? super T> queue;
//當該引用被加入到queue中的時候,該欄位被設定為queue中的下一個元素,以形成連結串列結構
volatile Reference next;
//在GC時,JVM底層會維護一個叫DiscoveredList的連結串列,存放的是Reference物件,discovered欄位指向的就是連結串列中的下一個元素,由JVM設定
transient private Reference<T> discovered;
//進行執行緒同步的鎖物件
static private class Lock { }
private static Lock lock = new Lock();
//等待加入queue的Reference物件,在GC時由JVM設定,會有一個java層的執行緒(ReferenceHandler)源源不斷的從pending中提取元素加入到queue
private static Reference<Object> pending = null;
}
複製程式碼
一個Reference物件的生命週期如下:
主要分為Native層和Java層兩個部分。
Native層在GC時將需要被回收的Reference物件加入到DiscoveredList中(程式碼在referenceProcessor.cpp
中process_discovered_references
方法),然後將DiscoveredList的元素移動到PendingList中(程式碼在referenceProcessor.cpp
中enqueue_discovered_ref_helper
方法),PendingList的隊首就是Reference類中的pending物件。 具體程式碼就不分析了,有興趣的同學可以看看這篇文章。
看看Java層的程式碼
private static class ReferenceHandler extends Thread {
...
public void run() {
while (true) {
tryHandlePending(true);
}
}
}
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
r = pending;
//如果是Cleaner物件,則記錄下來,下面做特殊處理
c = r instanceof Cleaner ? (Cleaner) r : null;
//指向PendingList的下一個物件
pending = r.discovered;
r.discovered = null;
} else {
//如果pending為null就先等待,當有物件加入到PendingList中時,jvm會執行notify
if (waitForNotify) {
lock.wait();
}
// retry if waited
return waitForNotify;
}
}
}
...
// 如果時CLeaner物件,則呼叫clean方法進行資源回收
if (c != null) {
c.clean();
return true;
}
//將Reference加入到ReferenceQueue,開發者可以通過從ReferenceQueue中poll元素感知到物件被回收的事件。
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
複製程式碼
流程比較簡單:就是源源不斷的從PendingList中提取出元素,然後將其加入到ReferenceQueue中去,開發者可以通過從ReferenceQueue中poll元素感知到物件被回收的事件。
另外需要注意的是,對於Cleaner型別(繼承自虛引用)的物件會有額外的處理:在其指向的物件被回收時,會呼叫clean方法,該方法主要是用來做對應的資源回收,在堆外記憶體DirectByteBuffer中就是用Cleaner進行堆外記憶體的回收,這也是虛引用在java中的典型應用。
看完了Reference的實現,再看看幾個實現類裡,各自有什麼不同。
SoftReference
public class SoftReference<T> extends Reference<T> {
static private long clock;
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;
}
}
複製程式碼
軟引用的實現很簡單,就多了兩個欄位:clock
和timestamp
。clock
是個靜態變數,每次GC時都會將該欄位設定成當前時間。timestamp
欄位則會在每次呼叫get方法時將其賦值為clock
(如果不相等且物件沒被回收)。
那這兩個欄位的作用是什麼呢?這和軟引用在記憶體不夠的時候才被回收,又有什麼關係呢?
這些還得看JVM的原始碼才行,因為決定物件是否需要被回收都是在GC中實現的。
size_t
ReferenceProcessor::process_discovered_reflist(
DiscoveredList refs_lists[],
ReferencePolicy* policy,
bool clear_referent,
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
VoidClosure* complete_gc,
AbstractRefProcTaskExecutor* task_executor)
{
...
//還記得上文提到過的DiscoveredList嗎?refs_lists就是DiscoveredList。
//對於DiscoveredList的處理分為幾個階段,SoftReference的處理就在第一階段
...
for (uint i = 0; i < _max_num_q; i++) {
process_phase1(refs_lists[i], policy,
is_alive, keep_alive, complete_gc);
}
...
}
//該階段的主要目的就是當記憶體足夠時,將對應的SoftReference從refs_list中移除。
void
ReferenceProcessor::process_phase1(DiscoveredList& refs_list,
ReferencePolicy* policy,
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
VoidClosure* complete_gc) {
DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
// Decide which softly reachable refs should be kept alive.
while (iter.has_next()) {
iter.load_ptrs(DEBUG_ONLY(!discovery_is_atomic() /* allow_null_referent */));
//判斷引用的物件是否存活
bool referent_is_dead = (iter.referent() != NULL) && !iter.is_referent_alive();
//如果引用的物件已經不存活了,則會去呼叫對應的ReferencePolicy判斷該物件是不時要被回收
if (referent_is_dead &&
!policy->should_clear_reference(iter.obj(), _soft_ref_timestamp_clock)) {
if (TraceReferenceGC) {
gclog_or_tty->print_cr("Dropping reference (" INTPTR_FORMAT ": %s" ") by policy",
(void *)iter.obj(), iter.obj()->klass()->internal_name());
}
// Remove Reference object from list
iter.remove();
// Make the Reference object active again
iter.make_active();
// keep the referent around
iter.make_referent_alive();
iter.move_to_next();
} else {
iter.next();
}
}
...
}
複製程式碼
refs_lists
中存放了本次GC發現的某種引用型別(虛引用、軟引用、弱引用等),而process_discovered_reflist
方法的作用就是將不需要被回收的物件從refs_lists
移除掉,refs_lists
最後剩下的元素全是需要被回收的元素,最後會將其第一個元素賦值給上文提到過的Reference.java#pending
欄位。
ReferencePolicy一共有4種實現:NeverClearPolicy,AlwaysClearPolicy,LRUCurrentHeapPolicy,LRUMaxHeapPolicy。其中NeverClearPolicy永遠返回false,代表永遠不回收SoftReference,在JVM中該類沒有被使用,AlwaysClearPolicy則永遠返回true,在referenceProcessor.hpp#setup
方法中中可以設定policy為AlwaysClearPolicy,至於什麼時候會用到AlwaysClearPolicy,大家有興趣可以自行研究。
LRUCurrentHeapPolicy和LRUMaxHeapPolicy的should_clear_reference方法則是完全相同:
bool LRUMaxHeapPolicy::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;
}
複製程式碼
timestamp_clock
就是SoftReference的靜態欄位clock
,java_lang_ref_SoftReference::timestamp(p)
對應是欄位timestamp
。如果上次GC後有呼叫SoftReference#get
,interval
值為0,否則為若干次GC之間的時間差。
_max_interval
則代表了一個臨界值,它的值在LRUCurrentHeapPolicy和LRUMaxHeapPolicy兩種策略中有差異。
void LRUCurrentHeapPolicy::setup() {
_max_interval = (Universe::get_heap_free_at_last_gc() / M) * SoftRefLRUPolicyMSPerMB;
assert(_max_interval >= 0,"Sanity check");
}
void LRUMaxHeapPolicy::setup() {
size_t max_heap = MaxHeapSize;
max_heap -= Universe::get_heap_used_at_last_gc();
max_heap /= M;
_max_interval = max_heap * SoftRefLRUPolicyMSPerMB;
assert(_max_interval >= 0,"Sanity check");
}
複製程式碼
其中SoftRefLRUPolicyMSPerMB
預設為1000,前者的計算方法和上次GC後可用堆大小有關,後者計算方法和(堆大小-上次gc時堆使用大小)有關。
看到這裡你就知道SoftReference到底什麼時候被被回收了,它和使用的策略(預設應該是LRUCurrentHeapPolicy),堆可用大小,該SoftReference上一次呼叫get方法的時間都有關係。
WeakReference
public class WeakReference<T> extends Reference<T> {
public WeakReference(T referent) {
super(referent);
}
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
複製程式碼
可以看到WeakReference在Java層只是繼承了Reference,沒有做任何的改動。那referent欄位是什麼時候被置為null的呢?要搞清楚這個問題我們再看下上文提到過的process_discovered_reflist
方法:
size_t
ReferenceProcessor::process_discovered_reflist(
DiscoveredList refs_lists[],
ReferencePolicy* policy,
bool clear_referent,
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
VoidClosure* complete_gc,
AbstractRefProcTaskExecutor* task_executor)
{
...
//Phase 1:將所有不存活但是還不能被回收的軟引用從refs_lists中移除(只有refs_lists為軟引用的時候,這裡policy才不為null)
if (policy != NULL) {
if (mt_processing) {
RefProcPhase1Task phase1(*this, refs_lists, policy, true /*marks_oops_alive*/);
task_executor->execute(phase1);
} else {
for (uint i = 0; i < _max_num_q; i++) {
process_phase1(refs_lists[i], policy,
is_alive, keep_alive, complete_gc);
}
}
} else { // policy == NULL
assert(refs_lists != _discoveredSoftRefs,
"Policy must be specified for soft references.");
}
// Phase 2:
// 移除所有指向物件還存活的引用
if (mt_processing) {
RefProcPhase2Task phase2(*this, refs_lists, !discovery_is_atomic() /*marks_oops_alive*/);
task_executor->execute(phase2);
} else {
for (uint i = 0; i < _max_num_q; i++) {
process_phase2(refs_lists[i], is_alive, keep_alive, complete_gc);
}
}
// Phase 3:
// 根據clear_referent的值決定是否將不存活物件回收
if (mt_processing) {
RefProcPhase3Task phase3(*this, refs_lists, clear_referent, true /*marks_oops_alive*/);
task_executor->execute(phase3);
} else {
for (uint i = 0; i < _max_num_q; i++) {
process_phase3(refs_lists[i], clear_referent,
is_alive, keep_alive, complete_gc);
}
}
return total_list_count;
}
void
ReferenceProcessor::process_phase3(DiscoveredList& refs_list,
bool clear_referent,
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
VoidClosure* complete_gc) {
ResourceMark rm;
DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
while (iter.has_next()) {
iter.update_discovered();
iter.load_ptrs(DEBUG_ONLY(false /* allow_null_referent */));
if (clear_referent) {
// NULL out referent pointer
//將Reference的referent欄位置為null,之後會被GC回收
iter.clear_referent();
} else {
// keep the referent around
//標記引用的物件為存活,該物件在這次GC將不會被回收
iter.make_referent_alive();
}
...
}
...
}
複製程式碼
不管是弱引用還是其他引用型別,將欄位referent置null的操作都發生在process_phase3
中,而具體行為是由clear_referent
的值決定的。而clear_referent
的值則和引用型別相關。
ReferenceProcessorStats ReferenceProcessor::process_discovered_references(
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
VoidClosure* complete_gc,
AbstractRefProcTaskExecutor* task_executor,
GCTimer* gc_timer) {
NOT_PRODUCT(verify_ok_to_handle_reflists());
...
//process_discovered_reflist方法的第3個欄位就是clear_referent
// Soft references
size_t soft_count = 0;
{
GCTraceTime tt("SoftReference", trace_time, false, gc_timer);
soft_count =
process_discovered_reflist(_discoveredSoftRefs, _current_soft_ref_policy, true,
is_alive, keep_alive, complete_gc, task_executor);
}
update_soft_ref_master_clock();
// Weak references
size_t weak_count = 0;
{
GCTraceTime tt("WeakReference", trace_time, false, gc_timer);
weak_count =
process_discovered_reflist(_discoveredWeakRefs, NULL, true,
is_alive, keep_alive, complete_gc, task_executor);
}
// Final references
size_t final_count = 0;
{
GCTraceTime tt("FinalReference", trace_time, false, gc_timer);
final_count =
process_discovered_reflist(_discoveredFinalRefs, NULL, false,
is_alive, keep_alive, complete_gc, task_executor);
}
// Phantom references
size_t phantom_count = 0;
{
GCTraceTime tt("PhantomReference", trace_time, false, gc_timer);
phantom_count =
process_discovered_reflist(_discoveredPhantomRefs, NULL, false,
is_alive, keep_alive, complete_gc, task_executor);
}
...
}
複製程式碼
可以看到,對於Soft references和Weak references clear_referent
欄位傳入的都是true,這也符合我們的預期:物件不可達後,引用欄位就會被置為null,然後物件就會被回收(對於軟引用來說,如果記憶體足夠的話,在Phase 1,相關的引用就會從refs_list中被移除,到Phase 3時refs_list為空集合)。
但對於Final references和 Phantom references,clear_referent
欄位傳入的是false,也就意味著被這兩種引用型別引用的物件,如果沒有其他額外處理,只要Reference物件還存活,那引用的物件是不會被回收的。Final references和物件是否重寫了finalize方法有關,不在本文分析範圍之內,我們接下來看看Phantom references。
PhantomReference
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,我們看個demo。
public static void demo() throws InterruptedException {
Object obj = new Object();
ReferenceQueue<Object> refQueue =new ReferenceQueue<>();
PhantomReference<Object> phanRef =new PhantomReference<>(obj, refQueue);
Object objg = phanRef.get();
//這裡拿到的是null
System.out.println(objg);
//讓obj變成垃圾
obj=null;
System.gc();
Thread.sleep(3000);
//gc後會將phanRef加入到refQueue中
Reference<? extends Object> phanRefP = refQueue.remove();
//這裡輸出true
System.out.println(phanRefP==phanRef);
}
複製程式碼
從以上程式碼中可以看到,虛引用能夠在指向物件不可達時得到一個'通知'(其實所有繼承References的類都有這個功能),需要注意的是GC完成後,phanRef.referent依然指向之前建立Object,也就是說Object物件一直沒被回收!
而造成這一現象的原因在上一小節末尾已經說了:對於Final references和 Phantom references,
clear_referent欄位傳入的時false,也就意味著被這兩種引用型別引用的物件,如果沒有其他額外處理,在GC中是不會被回收的。
對於虛引用來說,從refQueue.remove();
得到引用物件後,可以呼叫clear
方法強行解除引用和物件之間的關係,使得物件下次可以GC時可以被回收掉。
End
針對文章開頭提出的幾個問題,看完分析,我們已經能給出回答:
1.我們經常在網上看到軟引用的介紹是:在記憶體不足的時候才會回收,那記憶體不足是怎麼定義的?為什麼才叫記憶體不足?
軟引用會在記憶體不足時被回收,記憶體不足的定義和該引用物件get的時間以及當前堆可用記憶體大小都有關係,計算公式在上文中也已經給出。
2.網上對於虛引用的介紹是:形同虛設,與其他幾種引用都不同,虛引用並不會決定物件的生命週期。主要用來跟蹤物件被垃圾回收器回收的活動。真的是這樣嗎?
嚴格的說,虛引用是會影響物件生命週期的,如果不做任何處理,只要虛引用不被回收,那其引用的物件永遠不會被回收。所以一般來說,從ReferenceQueue中獲得PhantomReference物件後,如果PhantomReference物件不會被回收的話(比如被其他GC ROOT可達的物件引用),需要呼叫clear
方法解除PhantomReference和其引用物件的引用關係。
3.虛引用在Jdk中有哪些場景下用到了呢?
DirectByteBuffer中是用虛引用的子類Cleaner.java
來實現堆外記憶體回收的,後續會寫篇文章來說說堆外記憶體的裡裡外外。
Ps: 最近一直在找工作,所以半個多月沒寫文章,本來是想簡單寫下Java引用的幾個點的,但寫的時候才發現不把牽連到的知識點說清楚不行,所以又寫了這麼多。 希望自己能拿到一個滿意的offer!