學習如何有效地使用 SoftReference、WeakReference 和 PhantomReference 層級:初級 Peter Haggar(haggar@us.ibm.com) 進階軟體工程師,IBM 公司 2003 年 1 月 Java 2 平台引入了 java.lang.ref 包,其中包括的類可以讓您引用對象,而不將它們留在記憶體中。這些類還提供了與垃圾收集器(garbage collector)之間有限的互動。Peter Haggar 在本文中分析了 SoftReference、WeakReference 和 PhantomReference 類的功能和行為,並就這些類的使用給出了一些編程風格上的建議。 當在 Java 2 平台中首次引入 java.lang.ref 包(其中包含 SoftReference、WeakReference 和 PhantomReference 類)時,它的實用性顯然被過分誇大了。它包含的類可能是有用的,但這些類具有的某些局限性會使它們顯得不是很有吸引力,而且其應用程式也將特別局限於解決一類特定的問題。 垃圾收集概述 引用類的主要功能就是能夠引用仍可以被垃圾收集器回收的對象。在引入引用類之前,我們只能使用強引用(strong reference)。舉例來說,下面一行代碼顯示的就是強引用 obj: Object obj = new Object(); obj 這個引用將引用堆中儲存的一個對象。只要 obj 引用還存在,垃圾收集器就永遠不會釋放用來容納該對象的儲存空間。 當 obj 超出範圍或被顯式地指定為 null 時,垃圾收集器就認為沒有對這個對象的其它引用,也就可以收集它了。然而您還需要注意一個重要的細節:僅憑對象可以被收集並不意味著垃圾收集器的一次指定運行就能夠回收它。由於各種垃圾收集演算法有所不同,某些演算法會更頻繁地分析生存期較短的對象,而不是較老、生存期較長的對象。因此,一個可供收集的對象可能永遠也不會被回收。如果程式在垃圾收集器釋放對象之前結束,這種情況就可能會出現。因此,概括地說,您永遠無法保證可供收集的對象總是會被垃圾收集器收集。 這些資訊對於您分析引用類是很重要的。由於垃圾收集有著特定的性質,所以引用類實際上可能沒有您原來想像的那麼有用,儘管如此,它們對於特定問題來說還是很有用的類。軟引用(soft reference)、弱引用(weak reference)和虛引用(phantom reference)對象提供了三種不同的方式來在不妨礙收集的情況下引用堆對象。每種引用對象都有不同的行為,而且它們與垃圾收集器之間的互動也有所不同。此外,這幾個新的引用類都表現出比典型的強引用“更弱”的引用形式。而且,記憶體中的一個對象可以被多個引用(可以是強引用、軟引用、弱引用或虛引用)引用。在進一步往下討論之前,讓我們來看看一些術語: 強可及對象(strongly reachable):可以通過強引用訪問的對象。 軟可及對象(softly reachable):不是強可及對象,並且能夠通過軟引用訪問的對象。 弱可及對象(weakly reachable):不是強可及對象也不是軟可及對象,並且能夠通過弱引用訪問的對象。 虛可及對象(phantomly reachable):不是強可及對象、軟可及對象,也不是弱可及對象,已經結束的,可以通過虛引用訪問的對象。 清除:將引用對象的 referent 網域設定為 null,並將引用類在堆中引用的對象聲明為可結束的。 SoftReference 類 SoftReference 類的一個典型用途就是用於記憶體敏感的快取。SoftReference 的原理是:在保持對對象的引用時保證在 JVM 報告記憶體不足情況之前將清除所有的軟引用。關鍵之處在於,垃圾收集器在運行時可能會(也可能不會)釋放軟可及對象。對象是否被釋放取決於垃圾收集器的演算法以及垃圾收集器運行時可用的記憶體數量。 WeakReference 類 WeakReference 類的一個典型用途就是正常化映射(canonicalized mapping)。另外,對於那些生存期相對較長而且重新建立的開銷也不高的對象來說,弱引用也比較有用。關鍵之處在於,垃圾收集器運行時如果碰到了弱可及對象,將釋放 WeakReference 引用的對象。然而,請注意,垃圾收集器可能要運行多次才能找到並釋放弱可及對象。 PhantomReference 類 PhantomReference 類只能用於跟蹤對被引用對象即將進行的收集。同樣,它還能用於執行 pre-mortem 清除操作。PhantomReference 必須與 ReferenceQueue 類一起使用。需要 ReferenceQueue 是因為它能夠充當通知機制。當垃圾收集器確定了某個對象是虛可及對象時,PhantomReference 對象就被放在它的 ReferenceQueue 上。將 PhantomReference 對象放在 ReferenceQueue 上也就是一個通知,表明 PhantomReference 對象引用的對象已經結束,可供收集了。這使您能夠剛好在對象佔用的記憶體被回收之前採取行動。 垃圾收集器和引用互動 垃圾收集器每次運行時都可以隨意地釋放不再是強可及的對象佔用的記憶體。如果垃圾收集器發現了軟可及對象,就會出現下列情況: SoftReference 對象的 referent 域被設定為 null,從而使該對象不再引用 heap 對象。 SoftReference 引用過的 heap 對象被聲明為 finalizable。 當 heap 對象的 finalize() 方法被運行而且該對象佔用的記憶體被釋放,SoftReference 對象就被添加到它的 ReferenceQueue(如果後者存在的話)。 如果垃圾收集器發現了弱可及對象,就會出現下列情況: WeakReference 對象的 referent 域被設定為 null,從而使該對象不再引用 heap 對象。 WeakReference 引用過的 heap 對象被聲明為 finalizable。 當 heap 對象的 finalize() 方法被運行而且該對象佔用的記憶體被釋放時,WeakReference 對象就被添加到它的 ReferenceQueue(如果後者存在的話)。 如果垃圾收集器發現了虛可及對象,就會出現下列情況: PhantomReference 引用過的 heap 對象被聲明為 finalizable。 與軟引用和弱引用有所不同,PhantomReference 在堆對象被釋放之前就被添加到它的 ReferenceQueue。(請記住,所有的 PhantomReference 對象都必須用經過關聯的 ReferenceQueue 來建立。)這使您能夠在堆對象被回收之前採取行動。 請考慮清單 1 中的代碼。圖 1 說明了這段代碼的執行情況。 清單 1. 使用 WeakReference 及 ReferenceQueue 的範例程式碼 //Create a strong reference to an object MyObject obj = new MyObject(); //1 //Create a reference queue ReferenceQueue rq = new ReferenceQueue(); //2 //Create a weakReference to obj and associate our reference queue WeakReference wr = new WeakReference(obj, rq); //3 圖 1. 執行了清單 1 中行 //1、//2 和 //3 的代碼之後的物件版面配置 圖 1 顯示了每行代碼執行後各對象的狀態。行 //1 建立 MyObject 對象,而行 //2 則建立 ReferenceQueue 對象。行 //3 建立引用其引用對象 MyObject 的 WeakReference 對象,還建立它的 ReferenceQueue。請注意,每個對象引用(obj、rq 及 wr)都是強引用。要利用這些引用類,您必須取消對 MyObject 對象的強引用,方法是將 obj 設定為 null。前面說過,如果不這樣做,對象 MyObject 永遠都不會被回收,引用類的任何優點都會被削弱。 每個引用類都有一個 get() 方法,而 ReferenceQueue 類有一個 poll() 方法。get() 方法返回對被引用對象的引用。在 PhantomReference 上調用 get() 總是會返回 null。這是因為 PhantomReference 只用於跟蹤收集。poll() 方法返回已被添加到隊列中的引用對象,如果隊列中沒有任何對象,它就返回 null。因此,執行清單 1 之後再調用 get() 和 poll() 的結果可能是: wr.get(); //returns reference to MyObject rq.poll(); //returns null 現在我們假定垃圾收集器開始運行。由於 MyObject 對象沒有被釋放,所以 get() 和 poll() 方法將返回同樣的值;obj 仍然保持對該對象進行強引用。實際上,物件版面配置還是沒有改變,和圖 1 所示的差不多。然而,請考慮下面的代碼: obj = null; System.gc(); //run the collector 在這段代碼執行後,物件版面配置就 2 所示: 圖 2. obj = null; 和垃圾收集器運行後的物件版面配置 現在,調用 get() 和 poll() 將產生與前面不同的結果: wr.get(); //returns null rq.poll(); //returns a reference to the WeakReference object 這種情況表明,MyObject 對象(對它的引用原來是由 WeakReference 對象進行的)不再可用。這意味著垃圾收集器釋放了 MyObject 佔用的記憶體,從而使 WeakReference 對象可以被放在它的 ReferenceQueue 上。這樣,您就可以知道當 WeakReference 或 SoftReference 類的 get() 方法返回 null 時,就有一個對象被聲明為 finalizable,而且可能(不過不一定)被收集。只有當 heap 對象完全結束而且其記憶體被回收後,WeakReference 或 SoftReference 才會被放到與其關聯的 ReferenceQueue 上。清單 2 顯示了一個完整的可運行程式,它展示了這些原理中的一部分。這段代碼本身就頗具說明性,它含有很多注釋和列印語句,可以協助您理解。 清單 2. 展示引用類原理的完整程式 import java.lang.ref.*; class MyObject { protected void finalize() throws Throwable { System.out.println("In finalize method for this object: " + this); } } class ReferenceUsage { public static void main(String args[]) { hold(); release(); } public static void hold() { System.out.println("Example of incorrectly holding a strong " + "reference"); //Create an object MyObject obj = new MyObject(); System.out.println("object is " + obj); //Create a reference queue ReferenceQueue rq = new ReferenceQueue(); //Create a weakReference to obj and associate our reference queue WeakReference wr = new WeakReference(obj, rq); System.out.println("The weak reference is " + wr); //Check to see if it's on the ref queue yet System.out.println("Polling the reference queue returns " + rq.poll()); System.out.println("Getting the referent from the " + "weak reference returns " + wr.get()); System.out.println("Calling GC"); System.gc(); System.out.println("Polling the reference queue returns " + rq.poll()); System.out.println("Getting the referent from the " + "weak reference returns " + wr.get()); } public static void release() { System.out.println(""); System.out.println("Example of correctly releasing a strong " + "reference"); //Create an object MyObject obj = new MyObject(); System.out.println("object is " + obj); //Create a reference queue ReferenceQueue rq = new ReferenceQueue(); //Create a weakReference to obj and associate our reference queue WeakReference wr = new WeakReference(obj, rq); System.out.println("The weak reference is " + wr); //Check to see if it's on the ref queue yet System.out.println("Polling the reference queue returns " + rq.poll()); System.out.println("Getting the referent from the " + "weak reference returns " + wr.get()); System.out.println("Set the obj reference to null and call GC"); obj = null; System.gc(); System.out.println("Polling the reference queue returns " + rq.poll()); System.out.println("Getting the referent from the " + "weak reference returns " + wr.get()); } } 用途和風格 這些類背後的原理就是避免在應用程式執行期間將對象留在記憶體中。相反,您以軟引用、弱引用或虛引用的方式引用對象,這樣垃圾收集器就能夠隨意地釋放對象。當您希望儘可能減小應用程式在其生命週期中使用的堆記憶體大小時,這種用途就很有好處。您必須記住,要使用這些類,您就不能保留對對象的強引用。如果您這麼做了,那就會浪費這些類所提供的任何好處。 另外,您必須使用正確的編程風格以檢查收集器在使用對象之前是否已經回收了它,如果已經回收了,您首先必須重新建立該對象。這個過程可以用不同的編程風格來完成。選擇錯誤的風格會導致出問題。請考慮清單 3 中從 WeakReference 檢索被引用對象的代碼風格: 清單 3. 檢索被引用對象的風格 obj = wr.get(); if (obj == null) { wr = new WeakReference(recreateIt()); //1 obj = wr.get(); //2 } //code that works with obj 研究了這段代碼之後,請看看清單 4 中從 WeakReference 檢索被引用對象的另一種代碼風格: 清單 4. 檢索被引用對象的另一種風格 obj = wr.get(); if (obj == null) { obj = recreateIt(); //1 wr = new WeakReference(obj); //2 } //code that works with obj 請比較這兩種風格,看看您能否確定哪種風格一定可行,哪一種不一定可行。清單 3 中體現出的風格不一定在所有情況下都可行,但清單 4 的風格就可以。清單 3 中的風格不夠好的原因在於,if 塊的主體結束之後 obj 不一定是非空值。請考慮一下,如果垃圾收集器在清單 3 的行 //1 之後但在行 //2 執行之前運行會怎樣。recreateIt() 方法將重新建立該對象,但它會被 WeakReference 引用,而不是強引用。因此,如果收集器在行 //2 在重新建立的對象上施加一個強引用之前運行,對象就會丟失,wr.get() 則返回 null。 清單 4 不會出現這種問題,因為行 //1 重新建立了對象並為其指定了一個強引用。因此,如果垃圾收集器在該行之後(但在行 //2 之前)運行,該對象就不會被回收。然後,行 //2 將建立對 obj 的 WeakReference。在使用這個 if 塊之後的 obj 之後,您應該將 obj 設定為 null,從而讓垃圾收集器能夠回收這個對象以充分利用弱引用。清單 5 顯示了一個完整的程式,它將展示剛才我們描述的風格之間的差異。(要運行該程式,其運行目錄中必須有一個“temp.fil”檔案。 清單 5. 展示正確的和不正確的編程風格的完整程式。 import java.io.*; import java.lang.ref.*; class ReferenceIdiom { public static void main(String args[]) throws FileNotFoundException { broken(); correct(); } public static FileReader recreateIt() throws FileNotFoundException { return new FileReader("temp.fil"); } public static void broken() throws FileNotFoundException { System.out.println("Executing method broken"); FileReader obj = recreateIt(); WeakReference wr = new WeakReference(obj); System.out.println("wr refers to object " + wr.get()); System.out.println("Now, clear the reference and run GC"); //Clear the strong reference, then run GC to collect obj. obj = null; System.gc(); System.out.println("wr refers to object " + wr.get()); //Now see if obj was collected and recreate it if it was. obj = (FileReader)wr.get(); if (obj == null) { System.out.println("Now, recreate the object and wrap it in a WeakReference"); wr = new WeakReference(recreateIt()); System.gc(); //FileReader object is NOT pinned...there is no //strong reference to it. Therefore, the next //line can return null. obj = (FileReader)wr.get(); } System.out.println("wr refers to object " + wr.get()); } public static void correct() throws FileNotFoundException { System.out.println(""); System.out.println("Executing method correct"); FileReader obj = recreateIt(); WeakReference wr = new WeakReference(obj); System.out.println("wr refers to object " + wr.get()); System.out.println("Now, clear the reference and run GC"); //Clear the strong reference, then run GC to collect obj obj = null; System.gc(); System.out.println("wr refers to object " + wr.get()); //Now see if obj was collected and recreate it if it was. obj = (FileReader)wr.get(); if (obj == null) { System.out.println("Now, recreate the object and wrap it in a WeakReference"); obj = recreateIt(); System.gc(); //FileReader is pinned, this will not affect //anything. wr = new WeakReference(obj); } System.out.println("wr refers to object " + wr.get()); } } 總結 如果使用得當,引用類還是很有用的。然而,由於它們所依賴的垃圾收集器行為有時候無法預知,所以其實用性就會受到影響。能否有效地使用它們還取決於是否應用了正確的編程風格;關鍵在於您要理解這些類是如何?的以及如何對它們進行編程。 參考資料 Sam Borman 撰寫了一系列關於 IBM 垃圾收集器(IBM Garbage Collector)的有趣的文章。第 1 部分討論了對象地址分配;第 2 部分詳細描述了垃圾收集;第 3 部分描述了如何解釋 verbosegc 以及如何使用其中一些命令列參數。 Jonathan Amsterdam 在他的文章“Java References”(Dr. Dobb's Journal,2000 年 2 月)中解釋了引用的工作原理,並給出了一些實用的總結,使大家可以更容易地使用它們。(您可以從這個 DDJ 存檔頁購買這篇文章。) Jeff Friesen 在來自 JavaWorld(2002 年 1 月)的這篇文章中示範了如何使用 Reference Object API 來管理映象快取、在重要的對象不再是強可及時擷取通知以及執行結束後的清理工作。 您可以在 developerWorks Java 技術專區上找到數百篇 Java 編程參考資料。 關於作者 Peter Haggar 是 IBM 在北卡羅來納州的 Research Triangle Park 的一名進階軟體工程師,他還是 Practical Java Programming Language Guide(Addison-Wesley 出版)一書的作者。此外,他還發表了無數篇關於 Java 編程的文章。他有著廣泛的編程經驗,曾致力於開發工具、類庫和作業系統的相關工作。Peter 在 IBM 從事新興網際網路技術方面的工作,目前主要從事有關高效能 Web 服務方面的工作。Peter 經常在很多業界會議上作為技術發言人就 Java 技術發表言論。他已經為 IBM 工作了 15 年多,並獲得了 Clarkson University 的電腦科學學士學位。您可以通過 haggar@us.ibm.com 與他聯絡。 |