你不可不知的Java引用型別之——SoftReference原始碼詳解
定義
SoftReference是軟引用,其引用的物件在記憶體不足的時候會被回收。只有軟引用指向的物件稱為軟可達(softly-reachable)物件。
說明
垃圾回收器會在記憶體不足,經過一次垃圾回收後,記憶體仍舊不足的時候回收掉軟可達物件。在虛擬機器丟擲OOM之前,會保證已經清除了所有指向軟可達物件的軟引用。
如果記憶體足夠,並沒有規定回收軟引用的具體時間,所以在記憶體充足的情況下,軟引用物件也可能存活很長時間。
JVM會根據當前記憶體的情況來決定是否回收softly-reachable物件,但只要referent有強引用存在,該referent就一定不會被清理,因此SoftReference適合用來實現memory-sensitive caches。軟引用的回收策略在不同的JVM實現會略有不同。
另外,JVM不僅僅只會考慮當前記憶體情況,還會考慮軟引用所指向的referent最近使用情況和建立時間來綜合決定是否回收該referent。
一般而言,SoftReference物件會在垃圾回收器回收其內部referent後,才會被放入其註冊的引用佇列中(如果建立時註冊了的話)。
Soft reference objects, which are cleared at the discretion of the garbage collector in response to memory demand.
就是說,軟引用具體什麼時候回收最終還是由虛擬機器自己決定的,所以不同虛擬機器對軟引用的回收方式會有些不一樣。
SoftReference原始碼
public class SoftReference<T> extends Reference<T> {
/**
* 由垃圾回收器負責更新的時間戳
*/
static private long clock;
/**
* 在get方法呼叫時更新的時間戳,當虛擬機器選擇軟引用進行清理時,可能會參考這個欄位。
*/
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;
}
/**
* 返回引用指向的物件,如果referent已經被程式或者垃圾回收器清理,則返回null。
*/
public T get() {
T o = super.get();
if (o != null && this.timestamp != clock)
this.timestamp = clock;
return o;
}
}
SoftReference類內部程式碼很少,兩個成員變數,clock是一個靜態變數,是由垃圾回收器負責更新的時間戳,在JVM初始化時,會對變數clock進行初始化,同時,在JVM發生GC時,也會更新clock的值,所以clock會記錄上次GC發生的時間點。
timestamp是在建立和更新時更新的時間戳,將其更新為clock的值,垃圾回收器在回收軟引用物件時可能會參考timestamp。
SoftReference類有兩個建構函式,一個是不傳引用佇列,一個傳引用佇列。在建立時,都會更新timestamp,將其賦值為clock的值,get方法也並沒有什麼騷操作,只是簡單的呼叫 super.get() 並在返回值不為null時更新timestamp。
軟引用何時回收
前面說過,軟引用會在記憶體不足的時候進行回收,但是回收時並不會一次性全部回收,而是會使用一定的回收策略。
下面以最常用的虛擬機器HotSpot進行說明。下面是Oracle文件中的說明:
The default value is 1000 ms per megabyte, which means that a soft reference will survive (after the last strong reference to the object has been collected) for 1 second for each megabyte of free space in the heap
預設的生存週期為1000ms/Mb,舉個具體的栗子:
假設,堆記憶體為512Mb,並且可用記憶體為400Mb,我們建立一個object A,用軟引用建立一個引用A的快取物件cache,以及另一個object B 引用object A。此時,由於B持有A的強引用,所以物件A是強可達並且不會被垃圾回收器回收。
如果B被刪除了,那麼A僅剩下一個軟引用cache引用它,如果A在400s內沒有再次被強引用關聯,它將會在超時後被刪除。
下面是一個控制軟引用的栗子:
public class SoftRefTest {
public static class A{
}
public static class B{
private A strongRef;
public void setStrongRef(A ref) {
this.strongRef = ref;
}
}
public static SoftReference<A> cache;
public static void main(String[] args) throws InterruptedException{
//用一個A類例項的軟引用初始化cache物件
SoftRefTest.A instanceA = new SoftRefTest.A();
cache = new SoftReference<SoftRefTest.A>(instanceA);
instanceA = null;
// instanceA 現在是軟可達狀態,並且會在之後的某個時間被垃圾回收器回收
Thread.sleep(10000);
...
SoftRefTest.B instanceB = new SoftRefTest.B();
//由於cache僅持有instanceA的軟引用,所以無法保證instanceA仍然存活
instanceA = cache.get();
if (instanceA == null){
instanceA = new SoftRefTest.A();
cache = new SoftReference<SoftRefTest.A>(instanceA);
}
instanceB.setStrongRef(instanceA);
instanceA = null;
// instanceA現在與cache物件存在軟引用並且與B物件存在強引用,所以它不會被垃圾回收器回收
...
}
}
但是需要注意的是,被軟引用物件關聯的物件會自動被垃圾回收器回收,但是軟引用物件本身也是一個物件,這些建立的軟引用並不會自動被垃圾回收器回收掉,所以在之前一篇中說明裡的栗子裡,軟引用是不會被釋放掉的。
所以,你仍然需要手動去清理它們,否則也會導致OOM的產生,這裡也舉一個小栗子:
public class SoftReferenceTest{
public static class MyBigObject{
int[] data = new int[128];
}
public static int CACHE_www.taohuaqing178.com INITIAL_CAPACITY = 100_000;
// 靜態集合儲存軟引用,會導致這些軟引用物件本身無法被垃圾回收器回收
public static Set<SoftReference<MyBigObject>> cache = new HashSet<>(CACHE_INITIAL_CAPACITY);
public static void main(String[] args) {
for (int i = 0; i < 100_000; i++) {
MyBigObject obj = new MyBigObject();
cache.add(new SoftReference<>(obj));
if (i%10_000 =www.xgll521.com= 0){
System.out.println("size of cache:" + cache.size());
}
}
System.out.println("End");
}
}
使用的虛擬機器引數為:
-Xms4m -Xmx4m -Xmn2m
輸出如下:
size of cache:1
size of cache:10001
size of cache:20001
size of cache:30001
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
最終丟擲了OOM,但這裡的原因卻並不是Java heap space,而是GC overhead limit exceeded ,之所以會丟擲這個錯誤,是由於虛擬機器一直在不斷回收軟引用,回收進行的速度過快,佔用的cpu過大(超過98%),並且每次回收掉的記憶體過小(小於2%),導致最終丟擲了這個錯誤。
對於這裡,合適的處理方式是註冊一個引用佇列,每次迴圈之後將引用佇列中出現的軟引用物件從cache中移除。
public class SoftReferenceTest{
public static int removedSoftRefs = 0;
public static class MyBigObject{
int[] data = new int[128];
}
public static int CACHE_INITIAL_CAPACITY = 100_000;
// 靜態集合儲存軟引用,會導致這些軟引用物件本身無法被垃圾回收器回收
public static Set<SoftReference<MyBigObject>> cache = new HashSet<>(CACHE_INITIAL_CAPACITY);
public static ReferenceQueue<MyBigObject> referenceQueue = new ReferenceQueue<>();
public static void main(String[] args) {
for (int i = 0; i <www.ysyl157.com 100_000; i++) {
MyBigObject obj = new MyBigObject();
cache.add(new SoftReference<>(obj, referenceQueue));
clearUselessReferences();
}
System.out.println("End, removed soft references=" + removedSoftRefs);
}
public static void clearUselessReferences() {
Reference<? extends MyBigObject> ref = referenceQueue.poll();
while (ref != null) yongshiyule178.com{
if (cache.remove(ref)) {
removedSoftRefs++;
}
ref = referenceQueue.poll();
}
}
}
使用同樣的虛擬機器配置,輸出如下:
End, removed soft references=97319
HotSpot虛擬機器對於軟引用的處理
就HotSpot虛擬機器而言,常用的回收策略是基於當前堆大小的LRU策略(LRUCurrentHeapPolicy),會使用clock的值減去timestamp,得到的差值,就是這個軟引用被閒置的時間,如果閒置足夠長時間,就認為是可被回收的。
bool LRUCurrentHeapPolicy::should_clear_reference(oop p,
jlong timestamp_clock) {
jlong interval =www.tongqt178.com timestamp_clock - java_lang_ref_SoftReference::timestamp(p);
assert(interval >www.michenggw.com= 0, "Sanity check");
if(interval <= _max_interval) {
return false;
}
return true;
}
這裡 timestamp_clock 即SoftReference中clock的值,即上次GC時間。java_lang_ref_SoftReference::timestamp(p)可以獲取引用中timestamp的值。
那麼這個足夠長的時間 _max_interval是怎麼計算的呢?
void LRUCurrentHeapPolicy::setup() {
_max_interval = (Universe::get_heap_free_at_last_gc() / M) * SoftRefLRUPolicyMSPerMB;
assert(_max_interval >= 0,"Sanity check");
}
其中SoftRefLRUPolicyMSPerMB預設1000,所以可以看出這個回收時間與上次GC後的剩餘空間大小有關,可用空間越大,_max_interval就越大。
如果GC之後,堆的可用空間還很大的話,SoftReference物件可以長時間的在堆中而不被回收。反之,如果GC之後,只剩下很少的記憶體可用,那麼SoftReference物件便會很快進行回收。
SoftReference在一定程度上會影響垃圾回收,如果軟可達物件中對應的referent多次垃圾回收仍然不滿足釋放條件,那麼它會停留在堆的老年代,佔據很大部分空間,在JVM沒有丟擲OutOfMemoryError前,它有可能會導致頻繁的Full GC,會對效能有一定的影響。
小結
軟引用的具體回收時間與具體虛擬機器有關
軟引用中會在建立和呼叫get方法的時候更新內部timestamp,提供給虛擬機器回收時進行參考
hotspot虛擬機器對於軟引用使用的是LRU策略,回收時會根據軟引用被閒置的時間和當前記憶體綜合進行判斷
真正重要的東西,用眼睛是看不見的。