Java物件的強、軟、弱和虛引用原理+結合ReferenceQueue物件構造Java物件的快取記憶體器
轉自:http://blog.csdn.net/lyfi01/article/details/6415726
1.Java物件的強、軟、弱和虛引用
在JDK 1.2以前的版本中,若一個物件不被任何變數引用,那麼程式就無法再使用這個物件。也就是說,只有物件處於可觸及(reachable)狀態,程式才能使用它。這 就像在日常生活中,從商店購買了某樣物品後,如果有用,就一直保留它,否則就把它扔到垃圾箱,由清潔工人收走。一般說來,如果物品已經被扔到垃圾箱,想再 把它撿回來使用就不可能了。但有時候情況並不這麼簡單,你可能會遇到類似雞肋一樣的物品,食之無味,棄之可惜。這種物品現在已經無用了,保留它會佔空間,但是立刻扔掉它也不划算,因 為也許將來還會派用場。對於這樣的可有可無的物品,一種折衷的處理辦法是:如果家裡空間足夠,就先把它保留在家裡,如果家裡空間不夠,即使把家裡所有的垃圾清除,還是無法容納那些必不可少的生活用品,那麼再扔掉這些可有可無的物品。
在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.Java物件可及性的判斷
在很多時候,一個物件並不是從根集直接引用的,而是一個物件被其他物件引用,甚至同時被幾個物件所引用,從而構成一個以根集為頂的樹形結構。如圖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物件的快取記憶體器了。
- 本文介紹Java物件的強、軟、弱和虛引用的概念、應用及其在UML中的表示。
-
4.2如何使用WeakHashMap
在Java集合中有一種特殊的Map型別—WeakHashMap,在這種Map中存放了鍵物件的弱引用,當一個鍵物件被垃圾回收器回收時,那麼相應的值物件的引用會從Map中刪除。WeakHashMap能夠節約儲存空間,可用來快取那些非必須存在的資料。關於Map介面的一般用法。
下面示例中MapCache類的main()方法建立了一個WeakHashMap物件,它存放了一組Key物件的弱引用,此外main()方法還建立了一個數組物件,它存放了部分Key物件的強引用。
- import java.util.WeakHashMap;
- class Element {
- private String ident;
- public Element(String id) {
- ident = id;
- }
- public String toString() {
- return ident;
- }
- publicint hashCode() {
- return ident.hashCode();
- }
- publicboolean equals(Object obj) {
- return obj instanceof Element && ident.equals(((Element) obj).ident);
- }
- protectedvoid finalize(){
- System.out.println("Finalizing "+getClass().getSimpleName()+" "+ident);
- }
- }
- class Key extends Element{
- public Key(String id){
- super(id);
- }
- }
- class Value extends Element{
- public Value (String id){
- super(id);
- }
- }
- publicclass CanonicalMapping {
- publicstaticvoid main(String[] args){
- int size=1000;
- Key[] keys=new Key[size];
- WeakHashMap map=new WeakHashMap();
- for(int i=0;i
- Key k=new Key(Integer.toString(i));
- Value v=new Value(Integer.toString(i));
- if(i%3==0)
- keys[i]=k;
- map.put(k, v);
- }
- System.gc();
- }
- }
從列印結果可以看出,當執行System.gc()方法後,垃圾回收器只會回收那些僅僅持有弱引用的Key物件。id可以被3整除的Key物件持有強引用,因此不會被回收。
4.3用 WeakHashMap 堵住洩漏
在 SocketManager 中防止洩漏很容易,只要用 WeakHashMap 代替 HashMap 就行了。(這裡假定SocketManager不需要執行緒安全)。當對映的生命週期必須與鍵的生命週期聯絡在一起時,可以使用這種方法。用WeakHashMap修復 SocketManager。
- publicclass SocketManager {
- private Map m = new WeakHashMap();
- publicvoid setUser(Socket s, User u) {
- m.put(s, u);
- }
- public User getUser(Socket s) {
- return m.get(s);
- }
- }
4.4配合使用引用佇列
WeakHashMap 用弱引用承載對映鍵,這使得應用程式不再使用鍵物件時它們可以被垃圾收集,get() 實現可以根據 WeakReference.get() 是否返回 null 來區分死的對映和活的對映。但是這只是防止 Map 的記憶體消耗在應用程式的生命週期中不斷增加所需要做的工作的一半,還需要做一些工作以便在鍵物件被收集後從 Map 中刪除死項。否則,Map 會充滿對應於死鍵的項。雖然這對於應用程式是不可見的,但是它仍然會造成應用程式耗盡記憶體。
引用佇列是垃圾收集器嚮應用程式返回關於物件生命週期的資訊的主要方法。弱引用有個建構函式取引用佇列作為引數。如果用關聯的引用佇列建立弱引用,在弱引用物件成為 GC 候選物件時,這個引用物件就在引用清除後加入到引用佇列中(具體參考上文軟引用示例)。
WeakHashMap 有一個名為 expungeStaleEntries() 的私有方法,大多數 Map 操作中會呼叫它,它去掉引用佇列中所有失效的引用,並刪除關聯的對映。
5.UML:使用關聯類指明特定形式的引用
關聯類能夠用來指明特定形式的引用,如弱(weak)、軟(soft)或虛 (phantom)引用。
3.4通過軟可及物件重獲方法實現Java物件的快取記憶體
利用Java2平臺垃圾收集機制的特性以及前述的垃圾物件重獲方法,我們通過一個僱員資訊查詢系統的小例子來說明如何構建一種快取記憶體器來避免重複構建同一個物件帶來的效能損失。我們將一個僱員的檔案資訊定義為一個Employee類:
- publicclass Employee {
- private String id;// 僱員的標識號碼
- private String name;// 僱員姓名
- private String department;// 該僱員所在部門
- private String Phone;// 該僱員聯絡電話
- privateint salary;// 該僱員薪資
- private String origin;// 該僱員資訊的來源
- // 構造方法
- public Employee(String id) {
- this.id = id;
- getDataFromlnfoCenter();
- }
- // 到資料庫中取得僱員資訊
- privatevoid getDataFromlnfoCenter() {
- // 和資料庫建立連線井查詢該僱員的資訊,將查詢結果賦值
- // 給name,department,plone,salary等變數
- // 同時將origin賦值為"From DataBase"
- }
- ……
這個Employee類的構造方法中我們可以預見,如果每次需要查詢一個僱員的資訊。哪怕是幾秒中之前剛剛查詢過的,都要重新構建一個例項,這是需要消耗很多時間的。下面是一個對Employee物件進行快取的快取器的定義:
- import java.lang.ref.ReferenceQueue;
- import java.lang.ref.SoftReference;
- import java.util.Hashtable;
- publicclass EmployeeCache {
- staticprivate EmployeeCache cache;// 一個Cache例項
- private Hashtable employeeRefs;// 用於Chche內容的儲存
- private ReferenceQueue q;// 垃圾Reference的佇列
- // 繼承SoftReference,使得每一個例項都具有可識別的標識。
- // 並且該標識與其在HashMap內的key相同。
- privateclass EmployeeRef extends SoftReference {
- private String _key = "";
- public EmployeeRef(Employee em, ReferenceQueue q) {
- super(em, q);
- _key = em.getID();
- }
- }
- // 構建一個快取器例項
- private EmployeeCache() {
- employeeRefs = new Hashtable();
- q = new ReferenceQueue();
- }
- // 取得快取器例項
- publicstatic EmployeeCache getInstance() {
- if (cache == null) {
- cache = new EmployeeCache();
- }
- return cache;
- }
- // 以軟引用的方式對一個Employee物件的例項進行引用並儲存該引用
- privatevoid cacheEmployee(Employee em) {
- cleanCache();// 清除垃圾引用
- EmployeeRef ref = new EmployeeRef(em, q);
- employeeRefs.put(em.getID(), ref);
- }
- // 依據所指定的ID號,重新獲取相應Employee物件的例項
- public Employee getEmployee(String ID) {
- Employee em = null;
- // 快取中是否有該Employee例項的軟引用,如果有,從軟引用中取得。
- if (employeeRefs.containsKey(ID)) {
- EmployeeRef ref = (EmployeeRef) employeeRefs.get(ID);
- em = (Employee) ref.get();
- }
- // 如果沒有軟引用,或者從軟引用中得到的例項是null,重新構建一個例項,
- // 並儲存對這個新建例項的軟引用
- if (em == null) {
- em = new Employee(ID);
- System.out.println("Retrieve From EmployeeInfoCenter. ID=" + ID);
- this.cacheEmployee(em);
- }
- return em;
- }
- // 清除那些所軟引用的Employee物件已經被回收的EmployeeRef物件
- privatevoid cleanCache() {
- EmployeeRef ref = null;
- while ((ref = (EmployeeRef) q.poll()) != null) {
- employeeRefs.remove(ref._key);
- }
- }
- // 清除Cache內的全部內容
- publicvoid clearCache() {
- cleanCache();
- employeeRefs.clear();
- System.gc();
- System.runFinalization();
- }
- }
4.使用弱引用構建非敏感資料的快取
4.1全域性 Map 造成的記憶體洩漏
無意識物件保留最常見的原因是使用Map將元資料與臨時物件(transient object)相關聯。假定一個物件具有中等生命週期,比分配它的那個方法呼叫的生命週期長,但是比應用程式的生命週期短,如客戶機的套接字連線。需要將一些元資料與這個套接字關聯,如生成連線的使用者的標識。在建立Socket時是不知道這些資訊的,並且不能將資料新增到Socket物件上,因為不能控制 Socket 類或者它的子類。這時,典型的方法就是在一個全域性 Map 中儲存這些資訊,如下面的 SocketManager 類所示:使用一個全域性 Map 將元資料關聯到一個物件。
- publicclass SocketManager {
- private Map m = new HashMap();
- publicvoid setUser(Socket s, User u) {
- m.put(s, u);
- }
- public User getUser(Socket s) {
- return m.get(s);
- }
- publicvoid removeUser(Socket s) {
- m.remove(s);
- }
- }
這種方法的問題是元資料的生命週期需要與套接字的生命週期掛鉤,但是除非準確地知道什麼時候程式不再需要這個套接字,並記住從 Map 中刪除相應的對映,否則,Socket 和 User 物件將會永遠留在 Map 中,遠遠超過響應了請求和關閉套接字的時間。這會阻止 Socket 和 User 物件被垃圾收集,即使應用程式不會再使用它們。這些物件留下來不受控制,很容易造成程式在長時間執行後記憶體爆滿。除了最簡單的情況,在幾乎所有情況下找出什麼時候 Socket 不再被程式使用是一件很煩人和容易出錯的任務,需要人工對記憶體進行管理。