作者:Vamei 出處:http://www.cnblogs.com/vamei 歡迎轉載,也請保留這段聲明。謝謝!
整個教程中已經不時的出現一些記憶體管理和記憶體回收的相關知識。這裡進行一個小小的總結。
Java是在JVM所虛擬出的記憶體環境中啟動並執行。記憶體分為棧(stack)和堆(heap)兩部分。我們將分別考察這兩個地區。
棧
棧的基本概念參考紙上談兵: 棧 (stack)。許多語言利用棧資料結構來記錄函數調用的次序和相關變數(參考Linux從程式到進程)。
在Java中,JVM中的棧記錄了線程的方法調用。每個線程擁有一個棧。在某個線程的運行過程中,如果有新的方法調用,那麼該線程對應的棧就會增加一個儲存單元,即幀(frame)。在frame中,儲存有該方法調用的參數、局部變數和返回地址。
調用棧
Java的參數和局部變數只能是基本類型的變數(比如int),或者對象的引用(reference)。因此,在棧中,只儲存有基本類型的變數和對象引用。
引用所指向的對象儲存在堆中。(引用可能為Null值,即不指向任何對象)
引用與對象
當被呼叫者法運行結束時,該方法對應的幀將被刪除,參數和局部變數所佔據的空間也隨之釋放。線程回到原方法,繼續執行。當所有的棧都清空時,程式也隨之運行結束。
堆
如上所述,棧(stack)可以自己照顧自己。但堆必須要小心對待。堆是JVM中一塊可自由分配給對象的地區。當我們談論記憶體回收(garbage collection)時,我們主要回收堆(heap)的空間。
Java的普通對象存活在堆中。與棧不同,堆的空間不會隨著方法調用結束而清空。因此,在某個方法中建立的對象,可以在方法調用結束之後,繼續存在於堆中。這帶來的一個問題是,如果我們不斷的建立新的對象,記憶體空間將最終消耗殆盡。
記憶體回收
記憶體回收(garbage collection,簡稱GC)可以自動清空堆中不再使用的對象。記憶體回收機制最早出現於1959年,被用於解決Lisp語言中的問題。記憶體回收是Java的一大特徵。並不是所有的語言都有記憶體回收功能。比如在C/C++中,並沒有記憶體回收的機制。程式員需要手動釋放堆中的記憶體。
由於不需要手動釋放記憶體,程式員在編程中也可以減少犯錯的機會。利用記憶體回收,程式員可以避免一些指標和記憶體泄露相關的bug(這一類bug通常很隱蔽)。但另一方面,記憶體回收需要耗費更多的計算時間。記憶體回收實際上是將原本屬於程式員的責任轉移給電腦。使用記憶體回收的程式需要更長的已耗用時間。
在Java中,對象的是通過引用使用的(把對象相像成致命的毒物,引用就像是用於提取毒物的鑷子)。如果不再有引用指向對象,那麼我們就再也無從調用或者處理該對象。這樣的對象將不可到達(unreachable)。記憶體回收用於釋放不可到達對象所佔據的記憶體。這是記憶體回收的基本原則。
(不可到達對象是死對象,是記憶體回收所要回收的垃圾)
早期的記憶體回收採用引用計數(reference counting)的機制。每個對象包含一個計數器。當有新的指向該對象的引用時,計數器加1。當引用移除時,計數器減1。當計數器為0時,認為該對象可以進行記憶體回收。
然而,一個可能的問題是,如果有兩個對象循環參考(cyclic reference),比如兩個對象互相引用,而且此時沒有其它(指向A或者指向B)的引用,我們實際上根本無法通過引用到達這兩個對象。
因此,我們以棧和static資料為根(root),從根出發,跟隨所有的引用,就可以找到所有的可到達對象。也就是說,一個可到達對象,一定被根引用,或者被其他可到達對象引用。
橙色,可到達;綠色,不可到達
JVM實施
JVM的記憶體回收是多種機制的混合。JVM會根據程式健全狀態,自行決定採用哪種記憶體回收。
我們先來瞭解"mark and sweep"。這種機制下,每個對象將有標記資訊,用於表示該對象是否可到達。當記憶體回收啟動時,Java程式暫停運行。JVM從根出發,找到所有的可到達對象,並標記(mark)。隨後,JVM需要掃描整個堆,找到剩餘的對象,並清空這些對象所佔據的記憶體。
另一種是"copy and sweep"。這種機制下,堆被分為兩個地區。對象總存活於兩個地區中的一個。當記憶體回收啟動時,Java程式暫停運行。JVM從根出發,找到可到達對象,將可到達對象複製到空白地區中並緊密排列,修改由於對象移動所造成的引用地址的變化。最後,直接清Null 物件原先存活的整個地區,使其成為新的空白地區。
可以看到,"copy and sweep"需要更加複雜的操作,但也讓對象可以緊密排列,避免"mark and sweep"中可能出現的空隙。在建立對象時,"copy and sweep"可以提供大塊的連續空間。因此,如果對象都比較"長壽",那麼適用於"mark and sweep"。如果對象的"新陳代謝"比較活躍,那麼適用於"copy and sweep"。
上面兩種機制是通過分代回收(generational collection)混合在一起的。每個對象記錄有它的世代(generation)資訊。所謂的世代,是指該對象所經曆的記憶體回收的次數。世代越久遠的對象,在記憶體中存活的時間越久。
根據對Java程式的統計觀察,世代越久的對象,越不可能被記憶體回收(富人越富,窮人越窮)。因此,當我們在記憶體回收時,要更多關注那些年輕的對象。
現在,具體看一下JVM中的堆:
我們看到,堆分為三代。其中的永久世代(permanent generation)中存活的是Class對象。這些對象不會被記憶體回收。我們在RTTI中已經瞭解到,每個Class對象代表一個類,包含有類相關的資料與方法,並提供類定義的代碼。每個對象在建立時,都要參照相應的Class對象。每個對象都包含有指向其對應Class對象的引用。
年輕世代(young generation)和成熟世代(tenured generation)需要進行記憶體回收。年輕世代中的對象世代較近,而成熟世代中的對象世代較久。
世代
年輕世代進一步分為三個地區
eden(伊甸): 新生對象存活於該地區。新生對象指從上次GC後建立的對象。
新生對象生活於伊甸園
from, to: 這兩個地區大小相等,相當於copy and sweep中的兩個地區。
當建立對象無法放入eden區時,將出發minor collection。JVM採用copy and sweep的策略,將eden區與from區的可到達對象複製到to區。經過一次記憶體回收,eden區和from區清空,to區中則緊密的存放著存活對象。隨後,from區成為新的to區, to區成為新的from區。
如果進行minor collection的時候,發現to區放不下,則將部分對象放入成熟世代。另一方面,即使to區沒有滿,JVM依然會移動世代足夠久遠的對象到成熟世代。
如果成熟世代放滿對象,無法移入新的對象,那麼將觸發major collection。JVM採用mark and sweep的策略,對成熟世代進行記憶體回收。
總結
以上是對JVM記憶體管理的一個概述。實際上,JVM擁有眾多版本。不同版本實施的GC機制會有不小的差異。另一方面,Java本身並沒有規定JVM的GC實施方式。GC依然是JVM發展的一個熱點方向。我們可以預期JVM的GC機制在未來會發生許多變化。
參考連結
http://www.ibm.com/developerworks/java/library/j-jtp11253/
http://www.cs.princeton.edu/picasso/mats/HotspotOverview.pdf
http://www.cs.umd.edu/class/spring2008/cmsc433/lectures/gc.pdf
歡迎繼續閱讀“Java快速教程”系列文章