# 4.3 清除:收尾和垃圾收集
程序員都知道“初始化”的重要性,但通常忘記清除的重要性。畢竟,誰需要來清除一個`int`呢?但是對于庫來說,用完后簡單地“釋放”一個對象并非總是安全的。當然,Java可用垃圾收集器回收由不再使用的對象占據的內存。現在考慮一種非常特殊且不多見的情況。假定我們的對象分配了一個“特殊”內存區域,沒有使用`new`。垃圾收集器只知道釋放那些由`new`分配的內存,所以不知道如何釋放對象的“特殊”內存。為解決這個問題,Java提供了一個名為`finalize()`的方法,可為我們的類定義它。在理想情況下,它的工作原理應該是這樣的:一旦垃圾收集器準備好釋放對象占用的存儲空間,它首先調用`finalize()`,而且只有在下一次垃圾收集過程中,才會真正回收對象的內存。所以如果使用`finalize()`,就可以在垃圾收集期間進行一些重要的清除或清掃工作。
但也是一個潛在的編程陷阱,因為有些程序員(特別是在C++開發背景的)剛開始可能會錯誤認為它就是在C++中為“析構器”(Destructor)使用的`finalize()`——析構(清除)一個對象的時候,肯定會調用這個函數。但在這里有必要區分一下C++和Java的區別,因為C++的對象肯定會被清除(排開編程錯誤的因素),而Java對象并非肯定能作為垃圾被“收集”去。或者換句話說:
垃圾收集并不等于“析構”!
若能時刻牢記這一點,踩到陷阱的可能性就會大大減少。它意味著在我們不再需要一個對象之前,有些行動是必須采取的,而且必須由自己來采取這些行動。Java并未提供“析構器”或者類似的概念,所以必須創建一個原始的方法,用它來進行這種清除。例如,假設在對象創建過程中,它會將自己描繪到屏幕上。如果不從屏幕明確刪除它的圖像,那么它可能永遠都不會被清除。若在`finalize()`里置入某種刪除機制,那么假設對象被當作垃圾收掉了,圖像首先會將自身從屏幕上移去。但若未被收掉,圖像就會保留下來。所以要記住的第二個重點是:
我們的對象可能不會當作垃圾被收掉!
有時可能發現一個對象的存儲空間永遠都不會釋放,因為自己的程序永遠都接近于用光空間的臨界點。若程序執行結束,而且垃圾收集器一直都沒有釋放我們創建的任何對象的存儲空間,則隨著程序的退出,那些資源會返回給操作系統。這是一件好事情,因為垃圾收集本身也要消耗一些開銷。如永遠都不用它,那么永遠也不用支出這部分開銷。
## 4.3.1 `finalize()`用途何在
此時,大家可能已相信了自己應該將`finalize()`作為一種常規用途的清除方法使用。它有什么好處呢?
要記住的第三個重點是:
垃圾收集只跟內存有關!
也就是說,垃圾收集器存在的唯一原因是為了回收程序不再使用的內存。所以對于與垃圾收集有關的任何活動來說,其中最值得注意的是`finalize()`方法,它們也必須同內存以及它的回收有關。
但這是否意味著假如對象包含了其他對象,`finalize()`就應該明確釋放那些對象呢?答案是否定的——垃圾收集器會負責釋放所有對象占據的內存,無論這些對象是如何創建的。它將對`finalize()`的需求限制到特殊的情況。在這種情況下,我們的對象可采用與創建對象時不同的方法分配一些存儲空間。但大家或許會注意到,Java中的所有東西都是對象,所以這到底是怎么一回事呢?
之所以要使用`finalize()`,看起來似乎是由于有時需要采取與Java的普通方法不同的一種方法,通過分配內存來做一些具有C風格的事情。這主要可以通過“固有方法”來進行,它是從Java里調用非Java方法的一種方式(固有方法的問題在附錄A討論)。C和C++是目前唯一獲得固有方法支持的語言。但由于它們能調用通過其他語言編寫的子程序,所以能夠有效地調用任何東西。在非Java代碼內部,也許能調用C的`malloc()`系列函數,用它分配存儲空間。而且除非調用了`free()`,否則存儲空間不會得到釋放,從而造成內存“漏洞”的出現。當然,`free()`是一個C和C++函數,所以我們需要在`finalize()`內部的一個固有方法中調用它。
讀完上述文字后,大家或許已弄清楚了自己不必過多地使用`finalize()`。這個思想是正確的;它并不是進行普通清除工作的理想場所。那么,普通的清除工作應在何處進行呢?
## 4.3.2 必須執行清除
為清除一個對象,那個對象的用戶必須在希望進行清除的地點調用一個清除方法。這聽起來似乎很容易做到,但卻與C++“析構器”的概念稍有抵觸。在C++中,所有對象都會析構(清除)。或者換句話說,所有對象都“應該”析構。若將C++對象創建成一個本地對象,比如在棧中創建(在Java中是不可能的),那么清除或析構工作就會在“結束花括號”所代表的、創建這個對象的作用域的末尾進行。若對象是用`new`創建的(類似于Java),那么當程序員調用C++的`delete`命令時(Java沒有這個命令),就會調用相應的析構器。若程序員忘記了,那么永遠不會調用析構器,我們最終得到的將是一個內存“漏洞”,另外還包括對象的其他部分永遠不會得到清除。
相反,Java不允許我們創建本地(局部)對象——無論如何都要使用`new`。但在Java中,沒有`delete`命令來釋放對象,因為垃圾收集器會幫助我們自動釋放存儲空間。所以如果站在比較簡化的立場,我們可以說正是由于存在垃圾收集機制,所以Java沒有析構器。然而,隨著以后學習的深入,就會知道垃圾收集器的存在并不能完全消除對析構器的需要,或者說不能消除對析構器代表的那種機制的需要(而且絕對不能直接調用`finalize()`,所以應盡量避免用它)。若希望執行除釋放存儲空間之外的其他某種形式的清除工作,仍然必須調用Java中的一個方法。它等價于C++的析構器,只是沒后者方便。
`finalize()`最有用處的地方之一是觀察垃圾收集的過程。下面這個例子向大家展示了垃圾收集所經歷的過程,并對前面的陳述進行了總結。
```
//: Garbage.java
// Demonstration of the garbage
// collector and finalization
class Chair {
static boolean gcrun = false;
static boolean f = false;
static int created = 0;
static int finalized = 0;
int i;
Chair() {
i = ++created;
if(created == 47)
System.out.println("Created 47");
}
protected void finalize() {
if(!gcrun) {
gcrun = true;
System.out.println(
"Beginning to finalize after " +
created + " Chairs have been created");
}
if(i == 47) {
System.out.println(
"Finalizing Chair #47, " +
"Setting flag to stop Chair creation");
f = true;
}
finalized++;
if(finalized >= created)
System.out.println(
"All " + finalized + " finalized");
}
}
public class Garbage {
public static void main(String[] args) {
if(args.length == 0) {
System.err.println("Usage: \n" +
"java Garbage before\n or:\n" +
"java Garbage after");
return;
}
while(!Chair.f) {
new Chair();
new String("To take up space");
}
System.out.println(
"After all Chairs have been created:\n" +
"total created = " + Chair.created +
", total finalized = " + Chair.finalized);
if(args[0].equals("before")) {
System.out.println("gc():");
System.gc();
System.out.println("runFinalization():");
System.runFinalization();
}
System.out.println("bye!");
if(args[0].equals("after"))
System.runFinalizersOnExit(true);
}
} ///:~
```
上面這個程序創建了許多`Chair`對象,而且在垃圾收集器開始運行后的某些時候,程序會停止創建`Chair`。由于垃圾收集器可能在任何時間運行,所以我們不能準確知道它在何時啟動。因此,程序用一個名為`gcrun`的標記來指出垃圾收集器是否已經開始運行。利用第二個標記`f`,`Chair`可告訴`main()`它應停止對象的生成。這兩個標記都是在`finalize()`內部設置的,它調用于垃圾收集期間。
另兩個`static`變量——`created`以及`finalized`——分別用于跟蹤已創建的對象數量以及垃圾收集器已進行完收尾工作的對象數量。最后,每個`Chair`都有它自己的(非`static`)`int i`,所以能跟蹤了解它具體的編號是多少。編號為47的`Chair`進行完收尾工作后,標記會設為`true`,最終結束`Chair`對象的創建過程。
所有這些都在`main()`的內部進行——在下面這個循環里:
```
while(!Chair.f) {
new Chair();
new String("To take up space");
}
```
大家可能會疑惑這個循環什么時候會停下來,因為內部沒有任何改變`Chair.f`值的語句。然而,`finalize()`進程會改變這個值,直至最終對編號47的對象進行收尾處理。
每次循環過程中創建的`String`對象只是屬于額外的垃圾,用于吸引垃圾收集器——一旦垃圾收集器對可用內存的容量感到“緊張不安”,就會開始關注它。
運行這個程序的時候,提供了一個命令行參數`before`或者`after`。其中,`before`參數會調用`System.gc()`方法(強制執行垃圾收集器),同時還會調用`System.runFinalization()`方法,以便進行收尾工作。這些方法都可在Java 1.0中使用,但通過使用`after`參數而調用的`runFinalizersOnExit()`方法卻只有Java 1.1及后續版本提供了對它的支持(注釋③)。注意可在程序執行的任何時候調用這個方法,而且收尾程序的執行與垃圾收集器是否運行是無關的。
③:不幸的是,Java 1.0采用的垃圾收集器方案永遠不能正確地調用`finalize()`。因此,`finalize()`方法(特別是那些用于關閉文件的)事實上經常都不會得到調用。現在有些文章聲稱所有收尾模塊都會在程序退出的時候得到調用——即使到程序中止的時候,垃圾收集器仍未針對那些對象采取行動。這并不是真實的情況,所以我們根本不能指望`finalize()`能為所有對象而調用。特別地,`finalize()`在Java 1.0里幾乎毫無用處。
前面的程序向我們揭示出:在Java 1.1中,收尾模塊肯定會運行這一許諾已成為現實——但前提是我們明確地強制它采取這一操作。若使用一個不是`before`或`after`的參數(如`none`),那么兩個收尾工作都不會進行,而且我們會得到象下面這樣的輸出:
```
Created 47
Created 47
Beginning to finalize after 8694 Chairs have been created
Finalizing Chair #47, Setting flag to stop Chair creation
After all Chairs have been created:
total created = 9834, total finalized = 108
bye!
```
因此,到程序結束的時候,并非所有收尾模塊都會得到調用(注釋④)。為強制進行收尾工作,可先調用`System.gc()`,再調用`System.runFinalization()`。這樣可清除到目前為止沒有使用的所有對象。這樣做一個稍顯奇怪的地方是在調用`runFinalization()`之前調用`gc()`,這看起來似乎與Sun公司的文檔說明有些抵觸,它宣稱首先運行收尾模塊,再釋放存儲空間。然而,若在這里首先調用`runFinalization()`,再調用`gc()`,收尾模塊根本不會執行。
④:到你讀到本書時,有些Java虛擬機(JVM)可能已開始表現出不同的行為。
針對所有對象,Java 1.1有時之所以會默認為跳過收尾工作,是由于它認為這樣做的開銷太大。不管用哪種方法強制進行垃圾收集,都可能注意到比沒有額外收尾工作時較長的時間延遲。
- Java 編程思想
- 寫在前面的話
- 引言
- 第1章 對象入門
- 1.1 抽象的進步
- 1.2 對象的接口
- 1.3 實現方案的隱藏
- 1.4 方案的重復使用
- 1.5 繼承:重新使用接口
- 1.6 多態對象的互換使用
- 1.7 對象的創建和存在時間
- 1.8 異常控制:解決錯誤
- 1.9 多線程
- 1.10 永久性
- 1.11 Java和因特網
- 1.12 分析和設計
- 1.13 Java還是C++
- 第2章 一切都是對象
- 2.1 用引用操縱對象
- 2.2 所有對象都必須創建
- 2.3 絕對不要清除對象
- 2.4 新建數據類型:類
- 2.5 方法、參數和返回值
- 2.6 構建Java程序
- 2.7 我們的第一個Java程序
- 2.8 注釋和嵌入文檔
- 2.9 編碼樣式
- 2.10 總結
- 2.11 練習
- 第3章 控制程序流程
- 3.1 使用Java運算符
- 3.2 執行控制
- 3.3 總結
- 3.4 練習
- 第4章 初始化和清除
- 4.1 用構造器自動初始化
- 4.2 方法重載
- 4.3 清除:收尾和垃圾收集
- 4.4 成員初始化
- 4.5 數組初始化
- 4.6 總結
- 4.7 練習
- 第5章 隱藏實現過程
- 5.1 包:庫單元
- 5.2 Java訪問指示符
- 5.3 接口與實現
- 5.4 類訪問
- 5.5 總結
- 5.6 練習
- 第6章 類復用
- 6.1 組合的語法
- 6.2 繼承的語法
- 6.3 組合與繼承的結合
- 6.4 到底選擇組合還是繼承
- 6.5 protected
- 6.6 累積開發
- 6.7 向上轉換
- 6.8 final關鍵字
- 6.9 初始化和類裝載
- 6.10 總結
- 6.11 練習
- 第7章 多態性
- 7.1 向上轉換
- 7.2 深入理解
- 7.3 覆蓋與重載
- 7.4 抽象類和方法
- 7.5 接口
- 7.6 內部類
- 7.7 構造器和多態性
- 7.8 通過繼承進行設計
- 7.9 總結
- 7.10 練習
- 第8章 對象的容納
- 8.1 數組
- 8.2 集合
- 8.3 枚舉器(迭代器)
- 8.4 集合的類型
- 8.5 排序
- 8.6 通用集合庫
- 8.7 新集合
- 8.8 總結
- 8.9 練習
- 第9章 異常差錯控制
- 9.1 基本異常
- 9.2 異常的捕獲
- 9.3 標準Java異常
- 9.4 創建自己的異常
- 9.5 異常的限制
- 9.6 用finally清除
- 9.7 構造器
- 9.8 異常匹配
- 9.9 總結
- 9.10 練習
- 第10章 Java IO系統
- 10.1 輸入和輸出
- 10.2 增添屬性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File類
- 10.5 IO流的典型應用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 壓縮
- 10.9 對象序列化
- 10.10 總結
- 10.11 練習
- 第11章 運行期類型識別
- 11.1 對RTTI的需要
- 11.2 RTTI語法
- 11.3 反射:運行期類信息
- 11.4 總結
- 11.5 練習
- 第12章 傳遞和返回對象
- 12.1 傳遞引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只讀類
- 12.5 總結
- 12.6 練習
- 第13章 創建窗口和程序片
- 13.1 為何要用AWT?
- 13.2 基本程序片
- 13.3 制作按鈕
- 13.4 捕獲事件
- 13.5 文本字段
- 13.6 文本區域
- 13.7 標簽
- 13.8 復選框
- 13.9 單選鈕
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 視窗化應用
- 13.16 新型AWT
- 13.17 Java 1.1用戶接口API
- 13.18 可視編程和Beans
- 13.19 Swing入門
- 13.20 總結
- 13.21 練習
- 第14章 多線程
- 14.1 反應靈敏的用戶界面
- 14.2 共享有限的資源
- 14.3 堵塞
- 14.4 優先級
- 14.5 回顧runnable
- 14.6 總結
- 14.7 練習
- 第15章 網絡編程
- 15.1 機器的標識
- 15.2 套接字
- 15.3 服務多個客戶
- 15.4 數據報
- 15.5 一個Web應用
- 15.6 Java與CGI的溝通
- 15.7 用JDBC連接數據庫
- 15.8 遠程方法
- 15.9 總結
- 15.10 練習
- 第16章 設計模式
- 16.1 模式的概念
- 16.2 觀察器模式
- 16.3 模擬垃圾回收站
- 16.4 改進設計
- 16.5 抽象的應用
- 16.6 多重分發
- 16.7 訪問器模式
- 16.8 RTTI真的有害嗎
- 16.9 總結
- 16.10 練習
- 第17章 項目
- 17.1 文字處理
- 17.2 方法查找工具
- 17.3 復雜性理論
- 17.4 總結
- 17.5 練習
- 附錄A 使用非JAVA代碼
- 附錄B 對比C++和Java
- 附錄C Java編程規則
- 附錄D 性能
- 附錄E 關于垃圾收集的一些話
- 附錄F 推薦讀物