Java中的 finalize() 與c++的解構函式 分析
理解finalize()-解構函式的替代者
by Tim Gooch
在許多方面,Java 類似於 C++。Java 的語法非常類似於 C++,Java 有類、方法和資料成員;Java 的類有建構函式; Java 有異常處理。
但是,如果你使用過 C++ 會發現 Java 也丟掉一些可能是你熟悉的特性。這些特性之一就是解構函式。取代使用解構函式,Java 支援finalize() 方法。
在本文中,我們將描述 finalize() 與 C++ 解構函式的區別。另外,我們將建立一個簡單的 Applet 來演示 finalize() 是如何工作的。
最終的界限
與 Java 不同,C++ 支援區域性物件(基於棧)和全域性物件(基於堆)。因為這一雙重支援,C++ 也提供了自動構造和析構,這導致了對建構函式和解構函式的呼叫,(對於堆物件)就是記憶體的分配和釋放。
在 Java 中,所有物件都駐留在堆記憶體,因此區域性物件就不存在。結果,Java 的設計者覺得不需要解構函式(象 C++ 中所實現的)。
取而代之,Java 定義了一個特殊的方法叫做finalize() ,它提供了 C++ 解構函式的一些功能。但是,finalize() 並不完全與 C++ 的解構函式一樣,並可以假設它會導致一系列的問題。finalize() 方法作用的一個關鍵元素是 Java 的垃圾回收器。
垃圾回收器
在 C/C++、Pascal和其他幾種多種用途的程式語言中,開發者有責任在記憶體管理上發揮積極的作用。例如,如果你為一個物件或資料結構分配了記憶體,那麼當你不再使用它時必須釋放掉該記憶體。
在 Java 中,當你建立一個物件時,Java 虛擬機器(JVM)為該物件分配記憶體、呼叫建構函式並開始跟蹤你使用的物件。當你停止使用一個物件(就是說,當沒有對該物件有效的引用時),JVM 通過垃圾回收器將該物件標記為釋放狀態。
當垃圾回收器將要釋放一個物件的記憶體時,它呼叫該物件的finalize() 方法(如果該物件定義了此方法)。垃圾回收器以獨立的低優先順序的方式執行,只有當其他執行緒掛起等待該記憶體釋放的情況出現時,它才開始執行釋放物件的記憶體。(事實上,你可以呼叫System.gc() 方法強制垃圾回收器來釋放這些物件的記憶體。)
在以上的描述中,有一些重要的事情需要注意。首先,只有當垃圾回收器釋放該物件的記憶體時,才會執行finalize()。如果在 Applet 或應用程式退出之前垃圾回收器沒有釋放記憶體,垃圾回收器將不會呼叫finalize()。
其次,除非垃圾回收器認為你的 Applet 或應用程式需要額外的記憶體,否則它不會試圖釋放不再使用的物件的記憶體。換句話說,這是完全可能的:一個 Applet 給少量的物件分配記憶體,沒有造成嚴重的記憶體需求,於是垃圾回收器沒有釋放這些物件的記憶體就退出了。
顯然,如果你為某個物件定義了finalize() 方法,JVM 可能不會呼叫它,因為垃圾回收器不曾釋放過那些物件的記憶體。呼叫System.gc() 也不會起作用,因為它僅僅是給 JVM 一個建議而不是命令。
finalize() 有什麼優點呢?
如果finalize() 不是解構函式,JVM 不一定會呼叫它,你可能會疑惑它是否在任何情況下都有好處。事實上,在 Java 1.0 中它並沒有太多的優點。
根據 Java 文件,finalize() 是一個用於釋放非 Java 資源的方法。但是,JVM 有很大的可能不呼叫物件的finalize() 方法,因此很難證明使用該方法釋放資源是有效的。
Java 1.1 通過提供一個System.runFinalizersOnExit() 方法部分地解決了這個問題。(不要將這個方法與 Java 1.0 中的System.runFinalizations() 方法相混淆。)不象System.gc() 方法那樣,System.runFinalizersOnExit() 方法並不立即試圖啟動垃圾回收器。而是當應用程式或 Applet 退出時,它呼叫每個物件的finalize() 方法。
正如你可能猜測的那樣,通過呼叫System.runFinalizersOnExit() 方法強制垃圾回收器清除所有獨立物件的記憶體,當清除程式碼執行時可能會引起明顯的延遲。現在建立一個示例 Applet 來演示 Java 垃圾回收器和finalize() 方法是如何相互作用的。
回收垃圾
通過使用Java Applet Wizard 建立一個新的 Applet 開始。當提示這樣做時,輸入final_things作為 Applet 名,並選擇不要生成原始檔註釋。
接下來,在Java Applet Wizard 進行第三步,不要選擇多執行緒選項。在第五步之前,根據需要修改 Applet 的描述。
當你單擊Finish 後,Applet Wizard 將生成一個新的工作空間,併為該專案建立預設的 Java 檔案。從列表 A 中選擇適當的程式碼輸入(我們已經突出顯示了你需要輸入的程式碼)。
當你完成程式碼的輸入後,配置Internet 瀏覽器將System.out 的輸出資訊寫到Javalog.txt 檔案中。(在IE 選項對話方塊的高階頁面中選擇起用 Java Logging。)
編譯並執行該 Applet。然後,等待 Applet 執行(你將在狀態列中看到 Applet 已啟動的資訊),退出瀏覽器,並開啟Javalog.txt 檔案。你將會發現類似於下列行的資訊:
1000 things constructed
0 things finalized
正如你能夠看到的那樣,建立了1,000個物件仍然沒有迫使垃圾回收器開始回收空間,即使在 Applet 退出時也沒有物件被使用。
現在,刪除在stop() 方法第一行中的註釋符,起用System.gc() 方法。再次編譯並執行該 Applet ,等待 Applet 完成執行,並退出瀏覽器。當你再次開啟Javalog.txt 檔案,你將看到下列行:
1000 things constructed
963 things finalized
這次,垃圾回收器認為大多數物件未被使用,並將它們回收。按順序,當垃圾回收器開始釋放這些物件的記憶體時,JVM呼叫它們的finalize() 方法。
繼承finalize()?
順便,如果你在類中定義了finalize() ,它將不會自動呼叫基類中的方法。在我們討論了finalize() 與 C++ 的解構函式的不同點後,對這個結論不會驚訝,因為為某個類定製的清除程式碼另一個類不一定會需要。
如果你決定要通過派生一個類的finalize() 方法來呼叫基類中的finalize() 方法,你可以象其他繼承方法一樣處理。
protected void finalize()
{
super.finalize();
// other finalization code...
}
除了允許你控制是否執行清除操作外,這個技術還使你可以控制當前類的finalize() 方法何時執行。
結論
然而有益的是,Java 的自動垃圾回收器不會失去平衡。作為便利的代價,你不得不放棄對系統資源釋放的控制。不象 C++ 中的解構函式,Java Applet 不會自動執行你的類中的finalize() 方法。事實上,如果你正在使用 Java 1.0,即使你試圖強制它呼叫finalize() 方法,也不能確保將呼叫它。
因此,你不應當依靠finalize()來執行你的 Applet 和應用程式的資源清除工作。取而代之,你應當明確的清除那些資源或建立一個try...finally 塊(或類似的機制)來實現。
舉例說明:
- publicclass FinalizationDemo {
- publicstaticvoid main(String[] args) {
- Cake c1 = new Cake(1);
- Cake c2 = new Cake(2);
- Cake c3 = new Cake(3);
- c2 = c3 = null; //垃圾物件
- System.gc(); //Invoke the Java garbage collector 呼叫gc迫使垃圾回收器回收記憶體
- }
- }
- class Cake extends Object {
- privateint id;
- public Cake(int id) {
- this.id = id;
- System.out.println("Cake Object " + id + "is created");
- }
- protectedvoid finalize() throws java.lang.Throwable {
- super.finalize();
- System.out.println("Cake Object " + id + "is disposed");
- }
- }
當一個物件變成一個垃圾物件的時候,如果此物件的記憶體被回收,那麼就可以呼叫系統中定義的finalize方法來完成
結果執行:
- C:\1>java FinalizationDemo
- Cake Object 1is created //c1的引用任然存在
- Cake Object 2is created // 不再被引用,垃圾物件
- Cake Object 3is created //1.不再被引用,垃圾物件即“不可達” GC就有責任回收這些記憶體空間, 但不一定會立即回收這個物件
- Cake Object 3is disposed //2.呼叫gc強制垃圾回收機制,回收記憶體,此時才會呼叫物件的finalize方法
- Cake Object 2is disposed
finalize
方法名。Java 技術允許使用 finalize() 方法在垃圾收集器將物件從記憶體中清除出去之前做必要的清理工作。這個方法是由垃圾收集器在確定這個物件沒有被引用時對這個物件呼叫的。它是在 Object 類中定義的,因此所有的類都繼承了它。子類覆蓋 finalize() 方法以整理系統資源或者執行其他清理工作。finalize() 方法是在垃圾收集器刪除物件之前對這個物件呼叫的。
Java中所有類都從Object類中繼承finalize()方法。
當垃圾回收器(garbage colector)決定回收某物件時,就會執行該物件的finalize()方法。值得C++程式設計師注意的是,finalize()方法並不能等同與解構函式。Java中是沒有解構函式的。C++的解構函式是在物件消亡時執行的。由於C++沒有垃圾回收,物件空間手動回收,所以一旦物件用不到時,程式設計師就應當把它delete()掉。所以解構函式中經常做一些檔案儲存之類的收尾工作。但是在Java中很不幸,如果記憶體總是充足的,那麼垃圾回收可能永遠不會進行,也就是說filalize()可能永遠不被執行,顯然指望它做收尾工作是靠不住的。
那麼finalize()究竟是做什麼的呢?它最主要的用途是回收特殊渠道申請的記憶體。Java程式有垃圾回收器,所以一般情況下記憶體問題不用程式設計師操心。但有一種JNI(Java Native Interface)呼叫non-Java程式(C或C++),finalize()的工作就是回收這部分的記憶體。