標籤:
在C++中,在heap上指派至比在stack上指派至更加昂貴。程式需要找到合適的記憶體塊,再返回記憶體的地址。但是在Java中記憶體回收行程顯著地提高了在heap上指派至的速度。聽起來會有些怪,但是這就是Java記憶體回收行程工作的方式。而且這意味著Java中在heap上指派至幾乎跟其他語言在stack上指派至一樣快。
比如說,C++中heap像是一個院子,每個對象佔據自己的地盤。在一些JVM中,Java的heap更像是傳送帶,每次分配一個對象時,傳送帶就會往前進一點(而不需要尋找合適的記憶體空間)。
瞭解其他系統中記憶體回收時如何工作的對理解Java記憶體回收時有協助的。一個簡單卻低效的記憶體回收機制是引用計數。在這種機制中,每個對象都有一個應用計數值,每次有一個引用指向一個對象,對象的引用計數值加一。每次一個引用離開範圍或者被設定為null,對象的引用計數值減一。因此,管理引用計數值是很小但是在程式生命週期中一直存在的花費。記憶體回收行程遍曆對象,當發現有對象的引用計數值為0,這意味著程式再也無法操作這個對象,對象將會被回收。有一個缺點是幾個對象出現循環參考卻不被其他對象引用。這些對象需要被回收,但是這些對象的引用計數值都不為0。檢查循環參考需要記憶體回收行程做額外的工作。引用計數常用來解釋記憶體回收的工作原理,但是卻沒有被任何JVM實現使用。
在一些快速的記憶體回收機制中,記憶體回收不是基於引用計數。事實上,記憶體回收基於這樣一個事實:任何一個活躍的對象都可以通過stack或者static對象通過鏈式引用找到。這個鏈式引用可以經過很多個對象。因此如果從stack或者static對象開始,遍曆所有的引用,將會發現所有活躍的對象。對於每一發現的對象,都要繼續尋找所有它引用的對象,直到不能再發現新的對象。需要注意到,循環參考的問題已經被解決,他們都不會被發現,因此自動被回收了。
JVM使用一種適應性的記憶體回收機制,具體的機制與它當前所使用的變種有關。其中一種變種是停止-複製(stop and copy)。這意味在程式首先被停止(不存在一種後台回收機制),然後每一活躍的對象都從原來的heap中複製到新的heap中,所有需要被回收的對象都被拋棄在原先的heap中。因為所有活躍的對象都被複製到新的heap中,他們會被重新分配空間,緊密相連,使得新的heap所佔的空間較小,並卻允許新的對象在heap空間後面直接被分配。
當一個對象從一個地方移動到另一個地方的時候,所有指向那個對象的引用都需要被改變。從stack或者static對象發出的引用都可以直接改變,heap對象發出的引用會在隨後被改變。實際上在對象複製完成後,會建立一張表,表中建立舊地址到新地址的索引。隨後遍曆一遍heap對象,修改他們發出引用的地址。
有兩個問題會導致停止-複製(stop and copy)機制效率比較低。第一個是需要兩個heap,而且需要管理實際記憶體的兩倍。有些JVM通過對heap分塊,每次只複製一塊記憶體。
第二個問題來自於複製過程本身。當程式穩定以後,會有很少或者沒有垃圾產生。儘管這樣 ,停止-複製(stop and copy)機制仍然把所有記憶體從一個地方複製到另一個地方,很大程度上降低了程式的效能。
為瞭解決這個問題,一些JVM檢測到很少或沒有垃圾產生時,切換到另外一種機制(這裡體現了JVM記憶體回收機制的適應性)。這種機制稱為標記-交換(mark-and-sweep)。這種機制在Sun早期的JVM中一直被使用。對於一般情況,標記-交換器制速度很慢,但是當產生的垃圾很少或者沒有的時候,這種機制的速度較快。
停止-複製機制使用相同的邏輯:從stack或static對象出發,遍曆所有的引用來發現活躍的對象。每當發現一個活躍的對象,就給對象樹立一個標記。當遍曆結束的時候,所有活躍的對象都被標記完成。這時遍曆所有對象,回收所有沒被標記的對象。在這個過程中沒有發生複製。所以如果記憶體回收行程需要收縮heap,需要改變對象的位置,填補回收之後的空間。
Java記憶體回收工作原理