你不可不知的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_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策略,回收時會根據軟引用被閑置的時間和當前內存綜合進行判斷


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM