# 附錄E 關于垃圾收集的一些話
“很難相信Java居然能和C++一樣快,甚至還能更快一些。”
據我自己的實踐,這種說法確實成立。然而,我也發現許多關于速度的懷疑都來自一些早期的實現方式。由于這些方式并非特別有效,所以沒有一個模型可供參考,不能解釋Java速度快的原因。
我之所以想到速度,部分原因是由于C++模型。C++將自己的主要精力放在編譯期間“靜態”發生的所有事情上,所以程序的運行期版本非常短小和快速。C++也直接建立在C模型的基礎上(主要為了向后兼容),但有時僅僅由于它在C中能按特定的方式工作,所以也是C++中最方便的一種方法。最重要的一種情況是C和C++對內存的管理方式,它是某些人覺得Java速度肯定慢的重要依據:在Java中,所有對象都必須在內存“堆”里創建。
而在C++中,對象是在棧中創建的。這樣可達到更快的速度,因為當我們進入一個特定的作用域時,棧指針會向下移動一個單位,為那個作用域內創建的、以棧為基礎的所有對象分配存儲空間。而當我們離開作用域的時候(調用完畢所有局部構造器后),棧指針會向上移動一個單位。然而,在C++里創建“內存堆”(Heap)對象通常會慢得多,因為它建立在C的內存堆基礎上。這種內存堆實際是一個大的內存池,要求必須進行再循環(復用)。在C++里調用`delete`以后,釋放的內存會在堆里留下一個洞,所以再調用`new`的時候,存儲分配機制必須進行某種形式的搜索,使對象的存儲與堆內任何現成的洞相配,否則就會很快用光堆的存儲空間。之所以內存堆的分配會在C++里對性能造成如此重大的性能影響,對可用內存的搜索正是一個重要的原因。所以創建基于棧的對象要快得多。
同樣地,由于C++如此多的工作都在編譯期間進行,所以必須考慮這方面的因素。但在Java的某些地方,事情的發生卻要顯得“動態”得多,它會改變模型。創建對象的時候,垃圾收集器的使用對于提高對象創建的速度產生了顯著的影響。從表面上看,這種說法似乎有些奇怪——存儲空間的釋放會對存儲空間的分配造成影響,但它正是JVM采取的重要手段之一,這意味著在Java中為堆對象分配存儲空間幾乎能達到與C++中在棧里創建存儲空間一樣快的速度。
可將C++的堆(以及更慢的Java堆)想象成一個庭院,每個對象都擁有自己的一塊地皮。在以后的某個時間,這種“不動產”會被拋棄,而且必須復用。但在某些JVM里,Java堆的工作方式卻是頗有不同的。它更象一條傳送帶:每次分配了一個新對象后,都會朝前移動。這意味著對象存儲空間的分配可以達到非常快的速度。“堆指針”簡單地向前移至處女地,所以它與C++的棧分配方式幾乎是完全相同的(當然,在數據記錄上會多花一些開銷,但要比搜索存儲空間快多了)。
現在,大家可能注意到了堆事實并非一條傳送帶。如按那種方式對待它,最終就要求進行大量的頁交換(這對性能的發揮會產生巨大干擾),這樣終究會用光內存,出現內存分頁錯誤。所以這兒必須采取一個技巧,那就是著名的“垃圾收集器”。它在收集“垃圾”的同時,也負責壓縮堆里的所有對象,將“堆指針”移至盡可能靠近傳送帶開頭的地方,遠離發生(內存)分頁錯誤的地點。垃圾收集器會重新安排所有東西,使其成為一個高速、無限自由的堆模型,同時游刃有余地分配存儲空間。
為真正掌握它的工作原理,我們首先需要理解不同垃圾收集器(GC)采取的工作方案。一種簡單、但速度較慢的GC技術是引用計數。這意味著每個對象都包含了一個引用計數器。每當一個引用同一個對象連接起來時,引用計數器就會自增。每當一個引用超出自己的作用域,或者設為`null`時,引用計數就會自減。這樣一來,只要程序處于運行狀態,就需要連續進行引用計數管理——盡管這種管理本身的開銷比較少。垃圾收集器會在整個對象列表中移動巡視,一旦它發現其中一個引用計數成為0,就釋放它占據的存儲空間。但這樣做也有一個缺點:若對象相互之間進行循環引用,那么即使引用計數不是0,仍有可能屬于應收掉的“垃圾”。為了找出這種自引用的組,要求垃圾收集器進行大量額外的工作。引用計數屬于垃圾收集的一種類型,但它看起來并不適合在所有JVM方案中采用。
在速度更快的方案里,垃圾收集并不建立在引用計數的基礎上。相反,它們基于這樣一個原理:所有非死鎖的對象最終都肯定能回溯至一個引用,該引用要么存在于棧中,要么存在于靜態存儲空間。這個回溯鏈可能經歷了幾層對象。所以,如果從棧和靜態存儲區域開始,并經歷所有引用,就能找出所有活動的對象。對于自己找到的每個引用,都必須跟蹤到它指向的那個對象,然后跟隨那個對象中的所有引用,“跟蹤追擊”到它們指向的對象……等等,直到遍歷了從棧或靜態存儲區域中的引用發起的整個鏈接網路為止。中途移經的每個對象都必須仍處于活動狀態。注意對于那些特殊的自引用組,并不會出現前述的問題。由于它們根本找不到,所以會自動當作垃圾處理。
在這里闡述的方法中,JVM采用一種“自適應”的垃圾收集方案。對于它找到的那些活動對象,具體采取的操作取決于當前正在使用的是什么變體。其中一個變體是“停止和復制”。這意味著由于一些不久之后就會非常明顯的原因,程序首先會停止運行(并非一種后臺收集方案)。隨后,已找到的每個活動對象都會從一個內存堆復制到另一個,留下所有的垃圾。除此以外,隨著對象復制到新堆,它們會一個接一個地聚焦在一起。這樣可使新堆顯得更加緊湊(并使新的存儲區域可以簡單地抽離末尾,就象前面講述的那樣)。
當然,將一個對象從一處挪到另一處時,指向那個對象的所有引用(引用)都必須改變。對于那些通過跟蹤內存堆的對象而獲得的引用,以及那些靜態存儲區域,都可以立即改變。但在“遍歷”過程中,還有可能遇到指向這個對象的其他引用。一旦發現這個問題,就當即進行修正(可想象一個散列表將老地址映射成新地址)。
有兩方面的問題使復制收集器顯得效率低下。第一個問題是我們擁有兩個堆,所有內存都在這兩個獨立的堆內來回移動,要求付出的管理量是實際需要的兩倍。為解決這個問題,有些JVM根據需要分配內存堆,并將一個堆簡單地復制到另一個。
第二個問題是復制。隨著程序變得越來越“健壯”,它幾乎不產生或產生很少的垃圾。盡管如此,一個副本收集器仍會將所有內存從一處復制到另一處,這顯得非常浪費。為避免這個問題,有些JVM能偵測是否沒有產生新的垃圾,并隨即改換另一種方案(這便是“自適應”的緣由)。另一種方案叫作“標記和清除”,Sun公司的JVM一直采用的都是這種方案。對于常規性的應用,標記和清除顯得非常慢,但一旦知道自己不產生垃圾,或者只產生很少的垃圾,它的速度就會非常快。
標記和清除采用相同的邏輯:從棧和靜態存儲區域開始,并跟蹤所有引用,尋找活動對象。然而,每次發現一個活動對象的時候,就會設置一個標記,為那個對象作上“記號”。但此時尚不收集那個對象。只有在標記過程結束,清除過程才正式開始。在清除過程中,死鎖的對象會被釋放然而,不會進行任何形式的復制,所以假若收集器決定壓縮一個斷續的內存堆,它通過移動周圍的對象來實現。
“停止和復制”向我們表明這種類型的垃圾收集并不是在后臺進行的;相反,一旦發生垃圾收集,程序就會停止運行。在Sun公司的文檔庫中,可發現許多地方都將垃圾收集定義成一種低優先級的后臺進程,但它只是一種理論上的實驗,實際根本不能工作。在實際應用中,Sun的垃圾收集器會在內存減少時運行。除此以外,“標記和清除”也要求程序停止運行。
正如早先指出的那樣,在這里介紹的JVM中,內存是按大塊分配的。若分配一個大塊頭對象,它會獲得自己的內存塊。嚴格的“停止和復制”要求在釋放舊堆之前,將每個活動的對象從源堆復制到一個新堆,此時會涉及大量的內存轉換工作。通過內存塊,垃圾收集器通常可利用死塊復制對象,就象它進行收集時那樣。每個塊都有一個生成計數,用于跟蹤它是否依然“存活”。通常,只有自上次垃圾收集以來創建的塊才會得到壓縮;對于其他所有塊,如果已從其他某些地方進行了引用,那么生成計數都會溢出。這是許多短期的、臨時的對象經常遇到的情況。會周期性地進行一次完整清除工作——大塊頭的對象仍未復制(只是讓它們的生成計數溢出),而那些包含了小對象的塊會進行復制和壓縮。JVM會監視垃圾收集器的效率,如果由于所有對象都屬于長期對象,造成垃圾收集成為浪費時間的一個過程,就會切換到“標記和清除”方案。類似地,JVM會跟蹤監視成功的“標記與清除”工作,若內存堆變得越來越“散亂”,就會換回“停止和復制”方案。“自定義”的說法就是從這種行為來的,我們將其最后總結為:“根據情況,自動轉換停止和復制/標記和清除這兩種模式”。
JVM還采用了其他許多加速方案。其中一個特別重要的涉及裝載器以及JIT編譯器。若必須裝載一個類(通常是我們首次想創建那個類的一個對象時),會找到`.class`文件,并將那個類的字節碼送入內存。此時,一個方法是用JIT編譯所有代碼,但這樣做有兩方面的缺點:它會花更多的時間,若與程序的運行時間綜合考慮,編譯時間還有可能更長;而且它增大了執行文件的長度(字節碼比擴展過的JIT代碼精簡得多),這有可能造成內存頁交換,從而顯著放慢一個程序的執行速度。另一種替代辦法是:除非確有必要,否則不經JIT編譯。這樣一來,那些根本不會執行的代碼就可能永遠得不到JIT的編譯。
由于JVM對瀏覽器來說是外置的,大家可能希望在使用瀏覽器的時候從一些JVM的速度提高中獲得好處。但非常不幸,JVM目前不能與不同的瀏覽器進行溝通。為發揮一種特定JVM的潛力,要么使用內建了那種JVM的瀏覽器,要么只有運行獨立的Java應用程序。
- 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 推薦讀物