Java的垃圾回收器GC機制

1.Java中finalize()的作用一主要是清理那些對象(並非使用new)獲得了一塊“特殊”的內存區域。程序員可以用finalize()來操作。 程序員都瞭解初始化的重要性,但常常會忘記同樣也重要的清理工作。畢竟,誰需要清理一個int呢?但在使用程序庫時,把一個對象用完後就“棄之不顧”的做法並非總是安全的。當然,Java有垃圾回收器負責回收無用對象佔據的內存資源。但也有特殊情況:假定你的對象(並非使用new)獲得了一塊“特殊”的內存區域,由於垃圾回收器只知道釋放那些經由new分配的內存,所以它不知道該如何釋放該對象的這塊“特殊”內存區域,爲了應對這種情況,java允許在類中定義一個名爲finalize()的方法。它的工作原理“假定”是這樣的:一旦垃圾回收器準備好釋放對象佔用的存儲空間,將首先調用其finalize()的方法。並且在下一次垃圾回收動作發生時,纔會真正回收對象佔用的內存。所以要是你打算用finalize(),就能在垃圾回收時刻做一些重要的清理工作。注意這裏的finalize()並不是C++裏的析構.在C++中,對象一定會被銷燬,而在Java裏的對象卻並非總是被垃圾回收(1.對象可能不被垃圾回收;2.垃圾回收並並不等於“析構”)。

    2.垃圾回收只與內存有關。也就是說,使用垃圾回收器的唯一原因是爲了回收程序不再使用的內存。所以對於與垃圾回收有關的任何行爲來說(尤其是finalize()方法),它們也必須同內存及其回收有關。但這是否意味着要是對象中含有其他對象,finalize()就應該明確釋放那些對象呢?不,無論對象是如何創建的,垃圾回收器都會負責釋放對象佔據的所有內存。這就將對finalize()的需求限制到一種特殊情況,即通過某種創建對象方式以外的方式爲對象分配了存儲空間。不過,java中一切皆爲對象,那這種特殊情況是怎麼回事呢?由於在分配內存時可能採用了類似C語言中的做法,而非java中的通常做法。這種情況主要發生在使用“本地方法”的情況下,本地方法是一種在Java中調用非Java代碼的方式。在非java代碼中,也許會調用C的malloc()函數系列來分配存儲空間,而且除非了free()函數

    3.垃圾回收如何工作

“引用記數(reference counting)”是一種簡單但速度很慢的垃圾回收技術。每個對象都含有一個引用記數器,當有引用連接至對象時,引用計數加1。當引用離開作用域或被置爲null時,引用計數減1。雖然管理引用記數的開銷不大,但需要在整個程序生命週期中持續地開銷。垃圾回收器會在含有全部對象的列表上遍歷,當發現某個對象的引用計數爲0時,就釋放其佔用的空間。這種方法有個缺陷,如果對象之間存在循環引用,可能會出現“對象應該被回收,但引用計數卻不爲零”的情況。對垃圾回收器而言,定位這樣存在交互引用的對象組所需的工作量極大。引用記數常用來說明垃圾收集的工作方式,似乎從未被應用於任何一種Java虛擬機實現中。


  在一些更快的模式中,垃圾回收器並非基於引用記數技術。它們依據的思想是:對任何“活”的對象,一定能最終追溯到其存活在堆棧或靜態存儲區之中的引用。這個引用鏈條可能會穿過數個對象層次。由此,如果你從堆棧和靜態存儲區開始,遍歷所有的引用,就能找到所有“活”的對象。對於發現的每個引用,你必須追蹤它所引用的對象,然後是此對象包含的所有引用,如此反覆進行,直到“根源於堆棧和靜態存儲區的引用”所形成的網絡全部被訪問爲止。你所訪問過的對象必須都是“活”的。注意,這就解決了“存在交互引用的整體對象”的問題,這些對象根本不會被發現,因此也就被自動回收了。


  在這種方式下,Java虛擬機將採用一種“自適應”的垃圾回收技術。至於如何處理找到的存活對象,取決於不同的Java虛擬機實現。有一種作法名爲“停止——複製”(stop-and-copy)。這意味着,先暫停程序的運行,(所以它不屬於後臺回收模式),然後將所有存活的對象從當前堆複製到另一個堆,沒有被複制的全部都是垃圾。當對象被複制到新堆時,它們是一個挨着一個的,所以新堆保持緊湊排列,然後就可以按前述方法簡單、直接地分配新空間了。

  “標記——清掃”所依據的思路同樣是從堆棧和靜態存儲區出發,遍歷所有的引用,進而找出所有存活的對象。每當它找到一個存活對象,就會給對象設一個標記,這個過程中不會回收任何對象。只有全部標記工作完成的時候,清除動作纔會開始。在清處過程中,沒有標記的對象將被釋放,不會發生任何複製動作。所以剩下的堆空間是不連續的,垃圾回收器要是希望得到連續空間的話,就得重新整理剩下的對象。


  “停止——複製”的意思是這種垃圾回收方式不是在後臺進行的;相反,垃圾回收動作發生的同時,程序將會被暫停。在Sun 公司的文檔中你會發現,許多參考文獻將垃圾回收視爲低優先級的後臺進程,但事實上垃圾回收器並非以這種方式實現——至少Sun公司早期版本的Java虛擬機中並非如此。當可用內存數量較低時,Sun版中的垃圾回收器纔會被激活,同樣,“標記——清掃”工作也必須在程序暫停的情況下才能進行。


  如前文所述,這裏討論的Java虛擬機,內存分配單位是較大的“塊”。如果對象較大,它會佔用單獨的塊。嚴格來說,“停止——複製”要求你在釋放舊有對象之前,必須先把所有存活對象從舊堆複製到新堆,這將導致大量內存複製行爲。有了塊之後,垃圾回收器在回收的時候就可以往廢棄的塊裏拷貝對象了。每個塊都用相應的“代數(generation count)”記錄它是否還存活。通常,如果塊在某處被引用,其代數會增加;垃圾回收器將對上次回收動作之後新分配的塊進行整理。這對處理大量短命的臨時對象很有幫助。垃圾回收器會定期進行完整的清除動作——大型對象仍然不會被複制(只是其代數會增加),內含小型對象的那些塊則被複制並整理。Java虛擬機會進行監視,如果所有對象都很穩定,垃圾回收器的效率降低的話,就切換到“標記——清掃”方式;同樣, Java虛擬機會注意“標記——清掃”的效果,要是堆空間出現很多碎片,就會切換回“停止——複製”方式。這就是“自適應”技術。你可以給它個羅嗦的稱呼:“自適應的、分代的、停止——複製、標記——清掃”式垃圾回收器。


  Java虛擬機中有許多附加技術用以提升速度。尤其是與加載器操作有關的,被稱爲“即時”(Just-In-Time,JIT)編譯的技術。這種技術可以把程序全部或部分翻譯成本地機器碼(這本來是Java虛擬機的工作),程序運行速度因此得以提升。當需要裝載某個類(通常是在你爲該類創建第一個對象)時,編譯器會先找到其 .class 文件,然後將該類的字節碼裝入內存。此時,有兩種方案可供選擇。一種是就讓即時編譯器編譯所有代碼。但這種做法有兩個缺陷:這種加載動作散落在整個程序生命週期內,累加起來要花更多時間;並且會增加可執行代碼的長度(字節碼要比即時編譯器展開後的本地機器碼小很多),這將導致頁面調度,從而降低程序速度。另一種做法稱爲“惰性編譯(lazy uation)”,意思是即時編譯器只在必要的時候才編譯代碼。這樣,從不會被執行的代碼也許就壓根不會被JIT所編譯。新版JDK中的Java HotSpot技術就採用了類似方法,代碼每次被執行的時候都會做一些優化,所以執行的次數越多,它的速度就越快。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章