1. 程式人生 > >你不可不知的Java引用型別之——SoftReference原始碼詳解

你不可不知的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策略,回收時會根據軟引用被閒置的時間和當前記憶體綜合進行判斷
  
  真正重要的東西,用眼睛是看不見的。