java垃圾回收器的工作機制

鴨脖發表於2012-04-21
Bruce Eckel的《Thinking in java》前兩章中多次強調了這一點:C++中的memory leak在java
中是不會發生的,開發人員根本不用去考慮這個問題,這大大減少了開發的時間。這是因為java有
一個垃圾回收器,用來監視用new建立的所有物件,並辨別那些不會再被引用的物件,隨後,釋放
這些物件的記憶體空間,以便供其他新的物件使用。但是java的垃圾回收器是怎麼辨別的呢?Bruce
並沒有做相應的解釋。下面我們就這一點做一個深入的探究。

首先我們要說明一下java中為物件分配記憶體的方式。在java中一切都是物件,並且共享一個超類
Object。儘管一切都被看做物件,但是操縱的識別符號實際上是物件的一個“引用”。像String s 
= new String("hello");我們得到的s是一個引用,然後我們對它進行了初始化。引用可以獨立
存在,就像遙控器可以獨立於電視機存在一樣,但是你不能向它傳送訊息,否則便會返回常見的運
行時錯誤NullPointerException。java中用new操作符建立的物件都是分配在堆區,這裡的堆與
資料結構中的堆是兩回事,分配方式倒是類似於連結串列。堆的好處是:編譯器不需要知道儲存的資料
在堆裡存活多長時間,但是在堆疊中所有項的確切生命週期都必須被知道。C++看重的是效率,為
了追求最大的執行速度,物件的儲存空間和生命週期可以在編寫程式時確定,這可以通過將物件置
於堆疊和靜態儲存區域內實現。但如果是在堆中那麼是動態地,可能需要大量的時間在堆中查詢和
分配。但是java認為物件趨向於變得複雜,所以查詢和釋放儲存空間的開銷不會對物件的建立造成
大的衝擊。所以java完全採用了動態分配。但是這其中有一個特例,那就是基本型別。因為根據上
面的描述。顯然在堆中用new建立一個小的、簡單的變數往往不是很有效。因此對於這些基本型別
java採用了與c++相同的方法,同樣將他們儲存在堆疊中。但是為了在堆中也能儲存他們,也為了
在容器中能儲存基本資料型別(容器中存放的都是物件的引用,但是堆疊區的“物件”沒有引用)
,java還為基本型別提供了包裝器類,這或許能解決很多同學對於包裝器類的疑惑吧。

Java的堆更像一個傳送帶,每分配一個新物件,它就往前移動一格。這意味著物件儲存空間的分配
速度相當快。Java的“堆指標”只是簡單地移動到尚未分配的領域。也就是說,分配空間的時候,
“堆指標”只管依次往前移動而不管後面的物件是否還要被釋放掉。如果可用記憶體耗盡之前程式就
退出就再好不過了,這樣的話垃圾回收器壓根就不會被啟用。但是由於“堆指標”只管依次往前移
動,總有一天記憶體會被耗盡,垃圾回收器就開始釋放記憶體。

怎麼判斷某個物件該被回收呢?答案就是當堆疊或靜態儲存區沒有對這個物件的引用時,就表示程
序(員)對這個物件沒有興趣了,它就應該被回收了。有兩種方法來知道這個物件有沒有被引用:
第一種是遍歷堆上的物件找引用;第二種是遍歷堆疊或靜態儲存區的引用找物件。前者的實現叫做
“引用計數法”,意思就是當有引用連線至物件時,引用計數加1,當引用離開作用域或被置為
null時,引用計數減1,這種方法有個缺陷,如果物件之間存在迴圈引用,可能會出現“物件應該
被回收,但引用計數卻不為零”的情況。Java採用的是後者,在這種方式下,Java虛擬機器採用一
種“自適應”的垃圾回收技術,如何處理找到的存活物件(也就是說不是垃圾),Java有兩種方式
一、停止—複製(stop-and-copy):先暫停程式的執行,然後將所有存活的物件從當前堆複製到另
一個堆,沒有複製的全部都是垃圾。當物件被複制到新堆時,它們是一個挨著一個的,緊湊的。效
率很低:首先,得有兩個堆空間佔用率200%;其次,垃圾較少時,複製大量的活著的物件,是很大
的浪費。
二、 標記—清掃(mark-and-sweep):從對戰和靜態儲存區出發,遍歷所有的引用,進而找出所有
存活的物件,如果活著,就標記。只有全部標記完畢的時候,清理動作才開始。在清理的時候,沒
有標記的物件將會被釋放,不會發生任何膚質動作。但是剩下的對空間是不連續的,垃圾回收器要
是希望得到連續空間的話,就得重新整理剩下的物件。

【注意】“停止-複製”和“標記-清掃”無非就是:“在大量的垃圾中找乾淨的東西和在大量乾淨
的東西里找垃圾”。不同的環境用不同的方式,這樣做完全是為了提高效率。“停止—複製”的意
思是這種垃圾回收動作不是在後臺進行的;相反,垃圾回收動作發生的同時,程式將會被暫停。有
人將垃圾回收視為低優先順序的後臺程式,而事實上並不是這樣,當可用記憶體數量比較低的時候,
Sun版本的垃圾回收器就會暫停執行程式。同樣,“標記-清掃”工作也必須在程式暫停的情況下才
能進行。

【注】 在java虛擬機器中,記憶體分配是以較大的塊為單位的。每個塊內都用相應的代數
(generation count)來記錄它是否還存活。代數隨著引用的次數而增加。垃圾回收器將對上次回
收動作之後的新分配的塊進行整理。這對處理大量短命的臨時物件很有幫助。垃圾回收器會定期進
行完整的清理動作——大型物件仍然不會被複制(只是代數增加),內涵小型物件的那些塊則被複制
並整理。Java虛擬機器會進行監視,如果所有物件都很穩定,垃圾回收器的效率降低的話,就切換到
“標記—清掃”方式;同樣,java虛擬機器會追蹤“標記—清掃”的效果,要是堆空間出現很多碎片
,就會切換到“停止—複製”方式。這就是“自適應”技術。

所以,Java垃圾回收器是一種“自適應的、分代的、停止—複製、標記-清掃”式的垃圾回收器。

【注】文章內大部分為網上摘錄,地址較分散,作者也多次參考thinking in java的描述。總結
得甚是不足。如有高見,望不吝賜教。

相關文章