標籤:記憶體回收行程 java記憶體回收 清理 初始化 java清理
第五章 初始化與清理(二)5.5 清理:終結處理和記憶體回收
清理的工作常常被忽略,Java有記憶體回收行程負責回收無用對象佔據的記憶體資源。但也有特殊情況:假定對象(並非使用new)獲得了一塊”特殊”的記憶體地區,由於記憶體回收行程只知道釋放那些由new分配的記憶體,所以不知道如何釋放特殊記憶體。Java允許在類中定義一個名為finalize()的方法,工作原理”假定”是這樣的:一旦記憶體回收行程準備好釋放對象佔用的儲存空間,首先調用其finalize()方法,並且在下一次記憶體回收動作發生時,才會真正回收對象佔用的記憶體。
Java的finalize()和C++的解構函式有所不同,C++中對象一定會被銷毀(如果程式中沒有缺陷的話),而Java中對象並非總是被記憶體回收。即:1.對象可能不被記憶體回收。2.記憶體回收並不等於”析構”
Java並未提供”解構函式”或類似的概念,要做的類似的清理工作,必須自己動手建立一個執行清理工作的普通方法。當”記憶體回收”發生時(不能保證一定會發生),finalize()得到了調用,相應的工作就會進行,如果記憶體回收沒有發生,就不會被調用。
只要程式沒有瀕臨儲存空間用完的那一刻,對象佔用的控制項就總也得不到釋放,如果程式執行結束,並且記憶體回收行程一直都沒有釋放你建立的任何對象的儲存空間,隨著程式的退出,資源會全部還給作業系統。這個策略是恰當的,因為記憶體回收本身也佔用記憶體控制項,如果不使用,記憶體開銷會變小。
5.5.1 finalize()用途何在
記憶體回收有關的任何行為(尤其是finalize()方法),它們也必須同記憶體及回收有關。finalize()方法存在的意義是為了回收那些用new建立出來的對象之外的特殊儲存空間,是由於在分配記憶體時,可能採用了類似C語言中的做法,而非Java中的通常做法(new)。這種情況主要發生在使用”本地方法”的情況下,本地方法是一種在Java中調用非Java代碼的方式。本地方法目前只支援C和C++,但它們可以調用其他語言寫的代碼。
在非Java代碼中,也許會調用C的malloc()函數系列來分配儲存空間,而且除非調用free()函數,否則儲存空間將得不到釋放,從而造成記憶體流失。free()是C和C++中的函數,所以需要在finalize()中用本地方法調用它。
5.5.2 你必須實施清理
要清理一個對象,使用者必須在需要清理的時刻調用執行清理動作的方法。在C++中,所有對象都會被銷毀,都應該被銷毀。如果在C++中建立了一個局部對象(也就是在堆棧上建立,這在Java中行不通),此時的銷毀動作發生在以”右花括弧”為邊界的、此對象範圍的末尾處。如果對象是用new建立的,那麼當程式員調用C++的delete操作符(Java沒有這個命令),就會調用相應的解構函式。如果沒有調用delete,那永遠不會調用解構函式,這樣會出現記憶體流失。
Java中不允許建立局部對象,必須使用new建立對象。在Java中,也沒有釋放對象的delete,記憶體回收行程會幫你釋放儲存空間。
5.5.3 終結條件
通常不能指望finalize(),必須建立其他的”清理”方法,並明確地調用它們。finalize()還有一個又去的用法,並不依賴於每次都要對finalize進行調用,也就是對象終結條件的驗證。
當對某個對象不再感興趣–也就是它可以被清理了,這個對象應該處於某種狀態,使它佔用的記憶體可以被安全地釋放。下面的例子示範了finalize()可能的使用方法:
class Book { boolean checkedOut = false; Book(boolean checkOut) { checkedOut = checkOut; } void checkIn() { checkedOut = false; } protected void finalize() { if(checkedOut) System.out.println("Error : checked out"); }}public class Test { public static void main(String[] args) { Book novel = new Book(true); novel.checkIn(); new Book(true); System.gc(); }}
本例的終結條件時:所有Book對象在被當作記憶體回收前都應該被checkIn(),但是在new Book(true)這個對象沒有被checkIn,要是沒有finalize()來驗證終結條件,很難發現這種缺陷。
System.gc()用於強制進行和終結動作。
5.5.4 記憶體回收行程如何工作
記憶體回收行程對於提高對象建立速度有明顯的效果,Java虛擬機器在工作的時候,儲存空間的釋放會影響儲存空間的分配,由於記憶體回收行程的存在,Java從堆分配空間的速度可以和其他語言從堆棧上分配控制項的速度相媲美。
先瞭解其他系統中的記憶體回收機制將能協助我們更好的理解Java中的回收機制,引用記數是一種簡單但速度很慢的記憶體回收技術。每個對象都含有一個引用計數器,當有引用和對象串連的時候,引用記數加1,當引用離開範圍或被置為null時,引用記數減1。
記憶體回收行程會在含有全部對象的列表上遍曆,當發現某個對象的引用記數為0時,就釋放其佔用的空間(但是,引用技術模式經常會在計數值變為0的時候立即釋放對象)。如果對象之間存在迴圈飲用,可能會出現”對象應該被回收,但引用記數卻不為0”的情況。引用記數常用來說明垃圾收集的工作方式,但似乎從來未被應用與任何一種Java虛擬機器實現中。
在更快的一些模式中,記憶體回收行程並非基於引用記數技術,而是:對任何“活”的對象,一定能最終追溯到其存活在堆棧或靜態儲存區中的引用。如果從堆棧和靜態儲存區開始,遍曆所有的引用,就能找到所有”活”的對象,對於每個引用,必須追蹤和它關聯的對象,然後是此關聯對象的所有引用,反覆進行,直到全部被訪問。
在上述的方式下,Java虛擬機器將採用一種自適應的記憶體回收技術。其中有一種找到存活對象的方法名為停止-複製(stop-and-copy)。顯然這意味著先暫停程式的運行(不屬於後台回收模式),然後將所有存活的對象從當前堆複製到另外一個堆,沒有被複製的都是應當被回收的。
當把對象從一處搬到另外一處時,所有之鄉它的那些引用都必須修正。位於堆或靜態儲存區的引用可以直接被修正,但可能還有其他指向這些對象的引用,它們在遍曆的過程中才能被找到。對於這種複製式回收器而言,效率會降低。1.需要兩個堆來回倒騰,某些Java虛擬機器對此問題的處理方式是,按需從堆中分配幾塊較大的記憶體,複製動作發生在這些大塊記憶體之間。2.程式進入穩定點後,產生少量垃圾,但是複製式回收器還是會不停的複製,對於第二種情況,一些Java虛擬機器會進行檢查:要是沒有新的垃圾產生,就會切換到另一種工作模式(標記-清掃mark-and-sweep),Sun公司早期版本的Java虛擬機器使用了這種技術。對一般用途而言,”標記-清掃”方式速度相當慢,但是當只會產生少量垃圾甚至不會產生垃圾的時候,速度就很快了。
標記-清掃 所依據的思路同樣是從堆棧和靜態儲存區出發,遍曆所有的引用,進而找出所有存活的對象。每當找到一個存活的對象,就會給對象設一個標記,這個過程中不會回收任何對象。只有全部標記工作完成的時候,清理才會開始。在清理過程中,沒有標記的對象將全部被釋放,不會有複製動作發生。所剩下的空間是不連續的,記憶體回收行程要是希望得到連續的空間的話,就得重新整理剩下的對象。
停止-複製 的意思是這種記憶體回收機制不在後台進行。記憶體回收動作發生的時候,程式會被停止,Sun公司的文檔中,許多參考文獻將記憶體回收視為低優先順序的後台進程,但事實上早起Sun公司Java虛擬機器中並非在後台實現記憶體回收,而是當可用記憶體較少時,Sun版本的記憶體回收行程會暫停運行程式,同樣的標記-清掃工作也必須在程式暫停情況下才能進行。
在Java虛擬機器中,記憶體配置以較大的“塊”為單位,嚴格來說,“停止-複製”要求在釋放舊對象之前,必須先把所有存活對象從舊堆中複製到新堆,有了“塊”之後,記憶體回收行程在回收的時候可以將對象拷貝到廢棄的塊中,每個塊都用響應的代數(generation count)來記錄它是否還存活。記憶體回收行程會定期進行完整的清理動作–大型物件仍然不會被複製(只是其代數會增加),內涵小型對象的塊會被複製並整理。Java虛擬機器會進行監視,在“標記-清掃”和“停止-複製”之間切換,這就是“自適應”技術。
Java虛擬機器中有很多提高速度的附加技術,尤其是與載入器操作有關的,被稱為”即時(just-in-time,JIT)”編譯器的技術。它可以把程式全部或部分翻譯成本地機器碼(這本來是Java虛擬機器的工作),程式運行速度得到了提升。當需要裝在某個類時(通常是建立該類的第一個對象),編譯器會首先找到.class檔案,然後將該類的位元組碼裝入記憶體。接下來有兩種方案可供選擇:
- 讓即時編譯器編譯所有代碼,這種家在動作散落在整個程式聲明周期內,累加起來會花費更多的時間,並且會增加可執行代碼的長度。
- 另一種成為惰性評估(lazy evaluation),意思是即時編譯器只在必要的時候編譯代碼,這樣不會執行的代碼不會被JIT編譯。
Thinking In Java筆記(第五章 初始化與清理(二))