定義
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_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 == 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 < 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) {
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 = timestamp_clock - java_lang_ref_SoftReference::timestamp(p);
assert(interval >= 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策略,回收時會根據軟引用被閑置的時間和當前內存綜合進行判斷