使用WeakReference 與 ReferenceQueue 簡單實現弱引用緩存


本文介紹對象的強、軟、弱和虛引用的概念、應用及其在UML中的表示。 

1.對象的強、軟、弱和虛引用 
   在JDK 1.2以前的版本中,若一個對象不被任何變量引用,那么程序就無法再使用這個對象。也就是說,只有對象處於可觸及(reachable)狀態,程序才能使用它。從JDK 1.2版本開始,把對象的引用分為4種級別,從而使程序能更加靈活地控制對象的生命周期。這4種級別由高到低依次為:強引用、軟引用、弱引用和虛引用。圖1為對象應用類層次。 

圖1 

⑴強引用(StrongReference) 
    強引用是使用最普遍的引用。如果一個對象具有強引用,那垃圾回收器絕不會回收它。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題。 

⑵軟引用(SoftReference) 

    如果一個對象只具有軟引用,則內存空間足夠,垃圾回收器就不會回收它;如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存(下文給出示例)。 

軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。 

⑶弱引用(WeakReference) 

    弱引用與軟引用的區別在於:只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由於垃圾回收器是一個優先級很低的線程,因此不一定會很快發現那些只具有弱引用的對象。 

弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。 

⑷虛引用(PhantomReference) 

    “虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。 

虛引用主要用來跟蹤對象被垃圾回收器回收的活動。虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用隊列 (ReferenceQueue)聯合使用。當垃圾回收器准備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之 關聯的引用隊列中。 

ReferenceQueue queue = new ReferenceQueue (); 
PhantomReference pr = new PhantomReference (object, queue);

   程序可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。如果程序發現某個虛引用已經被加入到引用隊列,那么就可以在所引用的對象的內存被回收之前采取必要的行動。 

2.對象可及性的判斷 

在很多時候,一個對象並不是從根集直接引用的,而是一個對象被其他對象引用,甚至同時被幾個對象所引用,從而構成一個以根集為頂的樹形結構。如圖2所示 



  在這個樹形的引用鏈中,箭頭的方向代表了引用的方向,所指向的對象是被引用對象。由圖可以看出,從根集到一個對象可以由很多條路徑。比如到達對象5的路徑就有①-⑤,③-⑦兩條路徑。由此帶來了一個問題,那就是某個對象的可及性如何判斷: 

◆單條引用路徑可及性判斷:在這條路徑中,最弱的一個引用決定對象的可及性。 
◆多條引用路徑可及性判斷:幾條路徑中,最強的一條的引用決定對象的可及性。 

比如,我們假設圖2中引用①和③為強引用,⑤為軟引用,⑦為弱引用,對於對象5按照這兩個判斷原則,路徑①-⑤取最弱的引用⑤,因此該路徑對對象5的引用為軟引用。同樣,③-⑦為弱引用。在這兩條路徑之間取最強的引用,於是對象5是一個軟可及對象。 

3.使用軟引用構建敏感數據的緩存 

3.1 為什么需要使用軟引用 

    首先,我們看一個雇員信息查詢系統的實例。我們將使用一個Java語言實現的雇員信息查詢系統查詢存儲在磁盤文件或者數據庫中的雇員人事檔案信息。作為一個用戶,我們完全有可能需要回頭去查看幾分鍾甚至幾秒鍾前查看過的雇員檔案信息(同樣,我們在瀏覽WEB頁面的時候也經常會使用“后退”按鈕)。這時我們通常會有兩種程序實現方式:一種是把過去查看過的雇員信息保存在內存中,每一個存儲了雇員檔案信息的Java對象的生命周期貫穿整個應用程序始終;另一種是當用戶開始查看其他雇員的檔案信息的時候,把存儲了當前所查看的雇員檔案信息的Java對象結束引用,使得垃圾收集線程可以回收其所占用的內存空間,當用戶再次需要瀏覽該雇員的檔案信息的時候,重新構建該雇員的信息。很顯然,第一種實現方法將造成大量的內存浪費,而第二種實現的缺陷在於即使垃圾收集線程還沒有進行垃圾收集,包含雇員檔案信息的對象仍然完好地保存在內存中,應用程序也要重新構建一個對象。我們知道,訪問磁盤文件、訪問網絡資源、查詢數據庫等操作都是影響應用程序執行性能的重要因素,如果能重新獲取那些尚未被回收的Java對象的引用,必將減少不必要的訪問,大大提高程序的運行速度。 

3.2 如何使用軟引用 

    SoftReference的特點是它的一個實例保存對一個Java對象的軟引用,該軟引用的存在不妨礙垃圾收集線程對該Java對象的回收。也就是說,一旦SoftReference保存了對一個Java對象的軟引用后,在垃圾線程對這個Java對象回收前,SoftReference類所提供的get()方法返回Java對象的強引用。另外,一旦垃圾線程回收該Java對象之后,get()方法將返回null。 

看下面代碼: 

MyObject aRef = new?MyObject(); 
SoftReference aSoftRef=new SoftReference(aRef); 

此時,對於這個MyObject對象,有兩個引用路徑,一個是來自SoftReference對象的軟引用,一個來自變量aReference的強引用,所以這個MyObject對象是強可及對象。 

隨即,我們可以結束aReference對這個MyObject實例的強引用: 

aRef = null; 

   此后,這個MyObject對象成為了軟可及對象。如果垃圾收集線程進行內存垃圾收集,並不會因為有一個SoftReference對該對象的引用而始終保留該對象。Java虛擬機的垃圾收集線程對軟可及對象和其他一般Java對象進行了區別對待:軟可及對象的清理是由垃圾收集線程根據其特定算法按照內存需求決定的。也就是說,垃圾收集線程會在虛擬機拋出OutOfMemoryError之前回收軟可及對象,而且虛擬機會盡可能優先回收長時間閑置不用的軟可及對象,對那些剛剛構建的或剛剛使用過的“新”軟可反對象會被虛擬機盡可能保留。在回收這些對象之前,我們可以通過: 

MyObject anotherRef=(MyObject)aSoftRef.get(); 

重新獲得對該實例的強引用。而回收之后,調用get()方法就只能得到null了。 

3.3 使用ReferenceQueue清除失去了軟引用對象的SoftReference 

    作為一個Java對象,SoftReference對象除了具有保存軟引用的特殊性之外,也具有Java對象的一般性。所以,當軟可及對象被回收之后,雖然這個SoftReference對象的get()方法返回null,但這個SoftReference對象已經不再具有存在的價值,需要一個適當的清除機制,避免大量SoftReference對象帶來的內存泄漏。在java.lang.ref包里還提供了ReferenceQueue。如果在創建SoftReference對象的時候,使用了一個ReferenceQueue對象作為參數提供給SoftReference的構造方法,如: 

ReferenceQueue queue = new?ReferenceQueue(); 
SoftReference?ref=new?SoftReference(aMyObject, queue); 

    那么當這個SoftReference所軟引用的aMyOhject被垃圾收集器回收的同時,ref所強引用的SoftReference對象被列入ReferenceQueue。也就是說,ReferenceQueue中保存的對象是Reference對象,而且是已經失去了它所軟引用的對象的Reference對象。另外從ReferenceQueue這個名字也可以看出,它是一個隊列,當我們調用它的poll()方法的時候,如果這個隊列中不是空隊列,那么將返回隊列前面的那個Reference對象。 

在任何時候,我們都可以調用ReferenceQueue的poll()方法來檢查是否有它所關心的非強可及對象被回收。如果隊列為空,將返回一個null,否則該方法返回隊列中前面的一個Reference對象。利用這個方法,我們可以檢查哪個SoftReference所軟引用的對象已經被回收。於是我們可以把這些失去所軟引用的對象的SoftReference對象清除掉。常用的方式為: 

SoftReference ref = null; 

while ((ref = (EmployeeRef) q.poll()) != null) { 
   // 清除ref 


理解了ReferenceQueue的工作機制之后,我們就可以開始構造一個Java對象的高速緩存器了。 

3.4通過軟可及對象重獲方法實現Java對象的高速緩存 

    利用Java2平台垃圾收集機制的特性以及前述的垃圾對象重獲方法,我們通過一個雇員信息查詢系統的小例子來說明如何構建一種高速緩存器來避免重復構建同一個對象帶來的性能損失。我們將一個雇員的檔案信息定義為一個Employee類: 



Java代碼  收藏代碼
  1. package com.epkj.cache;  
  2.   
  3. public class Employee {  
  4.   
  5.     private String id;  
  6.       
  7.     private String name;  
  8.   
  9.     public Employee(String id) {  
  10.         this.id = id;  
  11.         this.name = String.valueOf(System.currentTimeMillis());  
  12.         //System.out.println("Employee.Employee() ..從數據庫中或者其他資源獲取對象");  
  13.     }  
  14.       
  15.     public String getId() {  
  16.         return id;  
  17.     }  
  18.   
  19.     public void setId(String id) {  
  20.         this.id = id;  
  21.     }  
  22.   
  23.     public String getName() {  
  24.         return name;  
  25.     }  
  26.   
  27.     public void setName(String name) {  
  28.         this.name = name;  
  29.     }  
  30.       
  31. }  



Java代碼  收藏代碼
  1. package com.epkj.cache;  
  2.   
  3. import java.lang.ref.ReferenceQueue;  
  4. import java.lang.ref.WeakReference;  
  5. import java.util.Collections;  
  6. import java.util.HashMap;  
  7. import java.util.Map;  
  8.   
  9. public class EmployeeCache {  
  10.   
  11.     //單例  
  12.     private static EmployeeCache cache;  
  13.       
  14.     //容器  
  15.     private Map<String, WeakEmployee> referent;  
  16.       
  17.     //引用隊列當SoftEmployee對象中的目標對象被銷毀后 會自定把SoftEmployee對象加入到該序列中  
  18.     //這樣就可以及時的清掉沒有目標對象的SoftEmployee  
  19.     private ReferenceQueue<Employee> queue;  
  20.       
  21.     //同步鎖  
  22.     private static Object lock = new Object();  
  23.       
  24.     //繼承SoftReference,實現對對象的軟引用  
  25.     //這個類所引用的目標對象會在JVM內存不足時自動回收  
  26.     private class WeakEmployee extends WeakReference<Employee> {  
  27.   
  28.         private String key;  
  29.           
  30.         public String getKey() {  
  31.             return key;  
  32.         }  
  33.   
  34.         public WeakEmployee(Employee referent, ReferenceQueue<Employee> queue) {  
  35.             super(referent, queue);  
  36.             this.key = referent.getId();  
  37.         }  
  38.           
  39.     }  
  40.       
  41.       
  42.     public synchronized Employee getEmployee(String id) {  
  43.         Employee e = null;  
  44.         if(referent.containsKey(id)) {  
  45.             e = referent.get(id).get();  
  46.         }  
  47.         if(e == null) {  
  48.             e = new Employee(id);  
  49.             cacheEmployee(e);  
  50.         }  
  51.         return e;  
  52.     }  
  53.       
  54.     //緩存對象  
  55.     private void cacheEmployee(Employee e) {  
  56.         cleanCache();// 清除垃圾引用  
  57.         WeakEmployee ref = new WeakEmployee(e, queue);  
  58.         referent.put(e.getId(), ref);  
  59.     }  
  60.       
  61.     //私有化構造參數  
  62.     private EmployeeCache() {  
  63.         this.referent = Collections.synchronizedMap(new HashMap<String, WeakEmployee>());  
  64.         this.queue = new ReferenceQueue<Employee>();  
  65.     }  
  66.       
  67.     //獲得實例  
  68.     public static EmployeeCache getInstance() {  
  69.         if(cache == null) {  
  70.             synchronized (lock) {  
  71.                 if(cache == null) {  
  72.                     cache = new EmployeeCache();  
  73.                 }  
  74.             }  
  75.         }  
  76.         return cache;  
  77.     }  
  78.       
  79.     //將SoftEmployee中目標元素為空的對象清除  
  80.     private void cleanCache() {  
  81.         WeakEmployee se = null;  
  82.         while((se = (WeakEmployee)queue.poll()) != null) {  
  83.             referent.remove(se.getKey());  
  84.             System.out.println("對象ID : " + se.getKey() + "已經被JVM回收");  
  85.         }  
  86.     }  
  87.       
  88.     public int getSize() {  
  89.         return referent.size();  
  90.     }  
  91.       
  92.     //清除緩存  
  93.     public void clearCache() {  
  94.         cleanCache();  
  95.         referent.clear();  
  96.     }  
  97.       
  98. }  



Java代碼  收藏代碼
    1. package com.epkj.cache;  
    2.   
    3. public class CacheTest {  
    4.   
    5.     public static void main(String[] args) throws InterruptedException {  
    6.         EmployeeCache cache = EmployeeCache.getInstance();  
    7.         for (int i = 0; i < 60000; i++) {  
    8.             cache.getEmployee(String.valueOf(i));  
    9.         }  
    10.     }  
    11.   
    12. }  


免責聲明!

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



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