Java 垃圾收集(Garbage Collection)

原文:http://www.artima.com/insidejvm/ed2/gcP.html

垃圾收集 ----Bill Venners

          Java虛擬機堆棧中存儲運行中的java應用程序創建的所有對象。這些對象在代碼裏由new,newarray,anewarray和multianewarray命令創建,但是代碼裏從不顯示的釋放這些創建的對象。Garbage Collection(gc程序)是一個運行於jvm上用來自動釋放那些不再被java運行程序引用的對象的後臺線程。

爲什麼需要GC

“garbage collection”(垃圾收集)表示那些不再被運行的程序所使用的對象(jvm裏的垃圾)是應該被丟棄的。一種更準確更現代的說法應該叫“內存回收”。如果一個對象不再被程序使用,那麼這個對象所佔用的內存空間就應該被回收,會收回後得到的內存可以被後續程序創建新對象時使用。Java垃圾回收器要判斷堆棧中哪些對象是不再被程序使用的,並回收這些對象所佔用的空間,留個程序後續使用。在回收不再被使用的對象的進程中,垃圾收集器要運行每一個正在被回收的對象的finalize方法。


另外垃圾收集時,垃圾回收器還要處理堆棧內存碎片問題。應用程序運行的過程中都會產生內存碎片,在一塊內存中新的對象被創建,同時不再使用的對象被回收,這樣被回收的內存就會夾雜在仍然被程序使用的對象所佔用的內存中間,這樣就產生了垃圾碎片。在堆棧中即使空閒的內存空間的總的大小足夠大,在爲新的對象分配內存是我們有時候還是需要使用擴展的空間。當內存中空閒的空間都不是連續的並且這些分散的空閒內存中找不到適合新對象的空間時就會出現這種情況。在內存系統中,如果不斷增長的堆中如果需要不斷增加額外的內存頁,那麼程序的性能就會下降。在內存有限的嵌入式系統中,內存碎片會導致不必要的程序內存不足(run out of memory)異常。


垃圾收集的第二個好處是保證程序的完整性,它是java安全策略的重要組成部分,java程序員不會因爲錯誤的內存釋放操作導致程序崩潰。


java垃圾收集的一個潛在的問題是它會影響程序的性能。JVM要在程序運行時跟蹤內存中被程序使用的對象,並找出不再被程序使用的對象進行內存回收,這些操作都會耗費cpu的時間,另外java程序員在垃圾收集環境下對cpu何時回收內存的控制權比較小。


垃圾收集算法

任何垃圾收集器都需要做兩件基本的事:1)檢測出哪些對象是不再被程序使用的對象 2)回收垃圾對象佔用的內存空間,並使這些空間可以被程序後續使用。


垃圾對象檢測是通過定義一組對象的根引用的集合,判斷是否可以通過根找到對象。任何一個對象,如果根引用中有路徑可以尋找到那個對象,那麼我們稱這個對象時活着的,否則就認爲這個對象死掉了,也就是垃圾對象,因爲一旦失去了一個對象的所有引用,那麼這個對象就不可能再被程序用到,它也就不可能對程序的後續執行產生任何影響。


對象根引用集合是由具體實現決定的,但是它一定會包含以下項:所有局部變量的引用;所有棧幀的操作數棧(operand stack of any stack frame);所有類的對象引用。另一個跟引用集合的來源是所有已加載的類的常量池中的所有對象的引用,比如string對象。這些常量池對象可能包含存儲在堆上的string對象,比如類名,超類名,接口名,屬性字段的名字,方法名,簽名的名字和方法的簽名等。根對象引用集合還可能包括被傳遞給本地方法(native method)的尚未被本地方法釋放的對象引用(這取決於本地方法接口,本地方法可能可以釋放傳遞進來的對象引用,比如通過方法返回釋放,通過顯示的回調釋放,或者是以上兩種方法的結合來釋放),任何從垃圾回收隊分配的JVM運行時數據域都是潛在的根引用集合組成部分。例如在某些方法實現中,方法內的對象可能是在垃圾回收堆上分配的,這樣可以允許同一個垃圾收集器檢測並釋放垃圾對象。


任何被根對象引用集合引用的對象都是可達的(可以通過跟引用在內存中找到該對象),所以這些對象都是“活着的”,另外被任何活着的對象引用的對象也是可達對象,程序還可以在後續的執行中訪問這些對象,所以這樣的對象必須在堆棧中保留,而不可達對象是程序後續執行中無法再訪問到的對象,所以是應該被回收的。


JVM是可以被實現的,這樣垃圾回收器可以知道真正的對象引用和看起來像對象引用的原生類型(比如int型,如果int型的變量被解釋爲一個本地指針,那麼它可能指向堆內存中的某個對象)之間的差別。然而有些垃圾回收器選擇不區分真正的對象引用和看起來像引用的原生類型,這樣的回收器被稱爲保守的回收器,他們有時候不會釋放所有的不再被使用的對象。有些時候垃圾對象可能被保守的垃圾回收器誤認爲任然“活着”,因爲有時候一個看似是對象引用的原生類型仍指向它。保守垃圾回收器在回收準確性上的損失卻一定程度上贏得了垃圾回收速度上的優勢。


兩種基本的檢測垃圾對象的方法是引用計數和跟蹤。引用計數垃圾回收器通過維持一個堆對象的計數器來判斷對象是否還“活着”,計數器記錄目前指向某個對象的引用有多少個。跟蹤回收器通過從根節點到實際對象的跟蹤圖來跟蹤一個對象,如果在跟蹤的路勁上遇到了對象就以某種方式對對象進行標記,那些沒有被標記的對象就是“不可達”的對象,它們應該被垃圾回收器回收。


引用計數回收器

引用計數是一種早期的垃圾回收策略,這種方法在堆棧上爲每個對象維持一個引用計數值,當創建一個對象並將該對象的引用賦值給一個變量的時候,這個引用計數的值就被初始化爲1,當這個對象的引用被賦值給其他變量的時候,計數值也相應的增加,任何一個引用計數值爲0的對象都是垃圾對象,應該被垃圾回收器回收。當一個對象A被回收時,任何一個被它引用的對象(B,C,D...)的引用計數值都應該減一,這可能使得某些(例如B)被它引用的對象也被回收,因爲對象A可能是最後一個持有B的引用的對象。

引用計數法的一個好處是計數回收器可以在很小的時間內完成垃圾收集,這樣不會長時間的打斷正在運行的主程序。這一特性使得它很適合實時系統(real-time environment),因爲實時系統程序不允許被長時間打斷。引用計數的一個缺點是它無法檢測循環引用,比如兩個或者兩個以上的對象之間相互引用(如:A持有B的引用,B持有C的引用,C又持有A的引用)。這樣的對象的引用計數值永遠不會爲0,即使他從根引用集合中已經沒有可以找到他們的路徑了。引用計數的另一個缺點是它需要一定的性能消耗花在引用計數值的增減上。由於引用計數法的這一固有的缺點,這一技術在現在已經很少使用了。現實使用中你更可能遇到的是跟蹤回收法。


跟蹤回收器

跟蹤回收器從根對象引用集合開始爲每個對象生成一個“跟蹤圖”,當垃圾回收器從根對象引用集合中按照某一路徑找到某個對象時,就以一種特定的方式對對象進行標記。這種標記方法可以是通過在對象本身上設置一個標籤,也可以通過是用單獨的位圖來記錄。當完成了對所有對象的標記後,那些沒有標記的對象就是垃圾對象,它們應該被回收器回收。有一個基本的跟蹤算法被稱作“標記清掃法”,這個名字表明算法分爲標記和清掃兩個階段。在標記階段,回收器從根對象引用集合開始遍歷所有路徑樹並標記遍歷過程中遇到的所有對象。在清掃階段,未被標記的對象被釋放,所得到的內存留作程序後續使用。在清掃階段JVM必須包含每個對象的終結(隱式調用對象的finalize方法)。

壓縮回收器

JVM的垃圾回收器會希望擁有一種解決內存碎片的方法,“標記清掃”垃圾回收器常用的兩種方法是壓縮和拷貝。這兩種方法都在程序運行的過程中通過移動內存中的對象來解決內存碎片問題。壓縮回收器將堆棧中任然活着的對象移動到堆的一端,同時將所有對活着的對象的引用更新爲對堆中新位置的對象的引用,這一操作完成後堆的另一端將會是大塊的連續空閒內存。

更新被移動的對象的引用有時候只是簡單的加一層間接引用。新的對象引用指向一個對象句柄表而不是直接直接引用堆棧上的對象。當一個對象被移動後只需要簡單的更新對象handler表的對象句柄指向新的內存位置(object handler),而運行中的程序的對象引用任然指向句柄表中的句柄----這些句柄是不會被移動的。這種方法可以簡化整理內存碎片的工作,但也增加了性能的開銷。

拷貝回收器

拷貝回收器將所有活着的對象移動到新的內存位置,在新位置對象被一個接一個的存放在相鄰的位置,這樣可以避免原來存儲對象的舊的內存位置中的空閒內存將對象分割開,拷貝後原來久的內存位置就都是空閒的內存空間了。拷貝回收的好處是對象可以輕易的根據根遍歷樹拷貝出來,省去了標記和清掃的過程。對象在程序運行的過程中被拷貝到新的位置,之前對象引用仍然留在原來的位置上,這些引用可以讓回收器輕易的檢測到那些對象是被移動過的,這樣垃圾回收器也可以輕易的將原對象的引用更新爲原來對象的拷貝的引用。

一個常見的的拷貝回收算法是“停止-複製”算法,這種方式下,堆棧被劃分爲兩個區域,其中的一個區域(可能先是A區域然後是B區域)允許程序在任何時候使用。程序在其中的一個區域A中爲對象分配內存,直到該區域的內存被耗盡,這個時候JVM停止正在執行的應用程序,然後遍歷堆棧,將已耗盡的內存區的所有活着的對象拷貝到另一個內存區B,當拷貝結束後應用程序恢復執行,之前被耗盡的那個內存區現在被認爲是空閒內存區,然後程序在B區域上新建對象,直到它被耗盡,然後重複上述停止-複製過程,只不過這次是將B中活着的對象拷貝到A區域。停止-拷貝回收法需要的內存空間的大小是分配給程序的堆棧大小的兩倍,因爲在程序運行的過程中只有一半的內存可以被程序使用。在下圖中你可以看到停止-拷貝回收法的圖解




這幅圖按時間順序展示了堆棧上的9個內存快照。在第一個快照中,內存的低地址那一半是沒有被程序使用的空間,而高地址的一半是被程序使用中的內存空間,其中部分已經被程序分配的對象佔用(用陰影標出),第二幅快照顯示內存逐漸被新分配的對象佔用,直到完全被佔用,如第三幅內存快照所示。這個時候垃圾回收器會停止程序的執行,並遍歷對象跟蹤樹,並將跟蹤到的活着的對象拷貝到低地址那一半的空間中,對象被一個緊鄰另一個的順序放到低地址的那一半內存中,如快照4所示。快照5顯示了垃圾回收完成後的內存狀況,這個時候高地址那一半內存空間就是空閒內存,而低地址那一半的部分被仍活着的對象佔用,快照6顯示低地址那一半逐漸被程序分配的新對象佔用的情況,直到完全被耗盡,如快照7所示,然後垃圾回收器會再次停止應用程序,接着將低地址那一半中仍活着的對象拷貝到高地址那一半的空間中一個接一個的緊鄰存放(快照8),最後回覆應用程序的執行,此時低地址的那一半空間又變成了空閒的內存。以後的回收過程會重複上述的過程。

分代回收器

停止-拷貝收集方法的一個缺點是每次都需要拷貝所有的活着的的對象。對於這一缺點,我們可以基於現實程序中常見的兩個現象進行改進

1:大部分被程序創建的對象只有很短的生命期

2:大部分程序會創建一些生命期很長的對象,影響停止-拷貝回收法效率的一個原因是回收器會一次又一次的複製生命期很長的那些對象

分代回收器通過按對象的年齡(從被創建到目前這個時刻的時間長短)對對象進行分組來解決這個問題,回收器回收年輕對象的頻率會更高。使用這種方法的時候堆棧會被劃分爲兩個活着多個子堆棧,每個子堆棧服務於一代對象,最年輕的那一代對象被回收的頻率最大,如果一個對象經歷幾輪迴收之後還活着,那麼它會被放入下一代對象的堆棧中,各子堆棧中的對象一代比一代老,他們的被回收的頻率也隨着對象變老而降低。標記清掃回收和拷貝回收都可以使用分代技術。

適應性回收器

自適應回收算法充分利用在不同的場景下回收算法的效率不同這一特性,它實時監控堆棧的情況,並根據目前的堆棧情況自行選擇合適的回收算法,它可能只是簡單的調整單個回收算法所使用的參數,也可能是從一個回收算法切換爲另一種,還可能是將堆棧劃分爲多個子堆棧並在不同的子堆棧上分別使用不同的回收算法。

使用自適應回收算法使得jvm的設計者不需要在多種回收算法中進行選擇,而只需將多種算法都應用到jvm上,讓jvm自行做出選擇。

火車算法

同手動釋放相比垃圾回收的一個潛在的缺點是程序員對回收垃圾使用的cpu時間的控制權變小了。一般垃圾回收器何時開始回收垃圾,回收所使用的時間有多長這些都是無法預測的。由於垃圾回收器在執行垃圾回收的時候經常停止整個應用程序,因此它可能導致應用程序在隨機的時刻停止程度隨機的時間,垃圾回收暫停程序也會使得實時應用程序無法及時的響應請求,而及時響應式實時

系統最根本的特性。如果垃圾回收算法可以使得暫停的時間長到能夠被使用者察覺或者使得程序不再適合實時系統那麼我們稱這樣的算法爲打斷性的,爲了最小化這一缺點,一個設計垃圾回收器的基本目標就是儘量最小化這種打斷性的特性甚至是完全消除它。


一種達到無中斷回收的方法是增量回收,增量回收器不是每次回收都去檢測並回收所有的垃圾對象,而只是檢測回收部分垃圾對象。因爲每次都只是檢測回收堆棧上的一部分垃圾,理論上垃圾回收的時間也就會短很多,一個應用增量回收法的垃圾回收器可以保證每次垃圾回收使用的時間不會大於某個固定值,這使得它可以被應用於實時系統中。這樣的回收器在普通的用戶環境中也是很受青睞的,因爲他可以消除垃圾收集所帶來的用戶可以察覺的長時間程序停頓。

火車回收算法最早由Richard Hudson 和 Eliot Moss提出,現在被用於Sun Hotspot virtual machine中,它爲分代回收器指定了一種成熟對象空間的組織方法。火車算法的目的是提供一種時間相關的成熟對象空間的增量回收。


車廂,火車和一個火車站(直譯)
火車算法在內存空間中將成熟對象空間劃分成固定大小的子空間塊,而這每個子空間都是每次垃圾回收器被調用時分開回收的(一次只回收其中一個子空間中的垃圾)。火車算法的名字來源於組織成熟對象子空間塊的方法。每一個子空間快屬於一個集合,集合內的子空間塊是有序的,而且所有集合自身也是有序的。這一算法的兩位作者爲了解釋他們的算法,將這些子空間塊稱爲”cars“,子空間塊組成的集合稱爲”trains“。在這一比喻中,成熟對象空間扮演火車站的角色。在同一個集合中的子空間塊是有序的,就像在在同一輛火車上的所有車廂是有序的一樣。而這些子空間塊組成的集合也是有序的,就像火車站的火車也是按照軌道號排序的一樣。
火車(車廂集合)按照他們的創建順序被賦予編號。所以火車站內,第一輛到達的火車進入1號軌道,被編號爲1號火車,下一輛進站的火車進入2號軌道,被編號爲2號火車,再下一輛進入3號軌道,編號爲3號火車如此類推。在這種編號方式下,數字越小就代表火車進站的時間越早(對象越老)。同一輛火車上每次車廂(子空間塊)都是加在車尾上最後一節車廂之後,第一個加在火車上的車廂編號爲1,下一個編號爲2,在下一個編號爲3以此類推。在同一火車上編號越小的車廂表明車廂越早被加到火車上(子空間塊越老),這一編號管理方式就使得所有的成熟對象空間中的子空間塊都是有序的。


圖9-2展示了三輛火車,編號分別爲1,2,3,1號火車有四節車廂,編號爲1.1-1.4,2號有三節車廂編號爲2.1-2.3,3號有五節車廂編號爲3.1-3.5.車廂1.1在車廂1.2之前,1.2在1.3之前,1號火車的最後一節車廂1.4在2號火車的第一節車廂2.1之前,同樣的2.3在3.1之前,每次調用火車回收算法都會回收一個子堆棧塊的空間,且只收集編號最小的那一個子堆棧塊的空間。因此,第一次運行火車回收算的時候,會回收圖9-2中編號爲1.1的子堆棧塊,下一次則會收集1.2,當編號爲1的火車的所有子堆棧塊全部收集完後再次運行火車回收算法則會去收集編號爲2.1的子堆棧塊,以此類推。

當年輕代堆棧空間的對象變老而沒有被回收的時候它會被放入成熟對象堆棧空間中。無論何時變老的對象被放入成熟對象堆棧空間的時候,它不會被放入編號最小的“火車”上,而是會放在其他已經存在的“火車”上,或者新建一個或者多個“火車”來存放他們。

回收車廂

每次調用火車回收算法的時候,垃圾回收器會回收編號最小的火車的最小編號“車廂”或者回收編號最小的整輛“火車”。算法首先檢查所有指向編號最小的火車的所有車廂的對象的引用,如果沒有外部的引用指向其中的任何“車廂”,那麼整列“火車”都會被回收。這一步驟使得火車算法可以回收那些無法放入單個子堆棧塊的大塊循環引用數據結構對象,因爲在下一個步驟中這些很大的循環引用數據結構對象必須被保證要被回收。


如果編號最小的“火車”被檢測出來全是垃圾,垃圾回收器會回收所有被“火車”對象佔用的空間,如果這輛“火車”上並不是全是垃圾對象,那麼算法會將注意力轉向編號最小的那節“車廂”,處理時,算法會移動或者釋放一些“車廂”上的對象,算法開始會將編號最小的這節“車廂”上尚有外部引用的對象移動到其他車廂,經過這個處理,車廂上剩下的對象就都是可以被回收的垃圾了,這個時候算法會回收這節“車廂”佔用的所有空間(釋放那些沒有外部引用的對象的操作仍然是在編號最小的那節“車廂”上執行的)。

確保循環引用數據結構對象在同一輛“火車”上被回收的關鍵是算法如何移動對象。如果一個對象在正在被回收的“車廂”上,且在成熟對象堆棧空間之外存在對它的引用,那麼這個對象會被移動到其他沒有被回收的“車廂”上。如果一個對象在成熟堆棧空間內被其他“火車”上的對象引用,那麼這個對象會被移動到引用它的那輛“火車”上,然後回到正在被回收的“火車”上繼續掃描尋找被這個被移動的對象引用的對象,任何被它引用的對象都會被移動到引用他們的那輛“火車”上。·然後同樣也會掃描被回收的火車上是否有新的被這些被移動的對象引用的對象,如果有也將他們移動到引用他們的“火車”上,遞歸的執行這個過程,直到沒有新的被引用的對象出現爲止。如果在這個過程中用來容納被移動的對象的“車廂”滿了,那麼算法會新建一個“車廂”來容納他們,並將這個車廂加在火車的車尾。

一旦在成熟對象空間外和成熟對象空間內的“火車”上不再存在對正在被回收的“車廂”上的對象的引用,那麼我們可以知道其他任何引用對正在被回收的“車廂”上的對象的對象都來自於同一輛火車的其他“車廂”。這時候算法會將這些對象移動到與這個正在被回收的“車廂”在同一編號最小的火車上的最後一個“車廂”上,然後回到正在被回收的“車廂”上尋找被這些移動的對象引用的新對象,如果找到就也將它們移動到引用他們的那節車廂上,重複這個過程直到沒有新的被引用的對象出現爲止,然後算法會回收被編號最小的那節車廂佔用的所有空間。

因此火車算法每次調用的時候都回收編號最小的那輛“火車”上編號最小的那節”車廂“活着回收編號最小的那一整輛“火車”。火車中很重要的一點是:即使循環引用數據結構對像非常大,大到單個的子堆棧塊(車廂)都無法容納,算法也必須保證它們最終會被回收。因爲對象會被移動到引用它們的“火車”上,所以相關聯的對象可能會被集中在一起。最終所有的循環引用數據結構對象都會被回收,無論它們有多大。

記憶集合和活躍的對象們

正像之前提到的,火車回收算法是爲了提供時間相關的成熟對象空間增量回收。由於可以指定子堆棧塊最大值並且每次調用回收器值回收一個子堆棧塊,火車回收算法大部分時候可以保證每次調用回收器進行回收所消耗的時間小於回收單個子堆棧塊的最大時間。然而不幸的是火車算法無法保證每次都是如此,因爲算法要做的事遠不止拷貝對象。

爲了加速來及回收的速度,火車算法利用了記憶集合,記憶集合是一個存放所有在“火車”外或”車廂“外但引用”火車“或”車廂“內的對象的對象,火車算法在成熟對象空間爲每一個”車廂“和沒一輛”火車“維護一個這樣的記憶集合。爲特定的”車廂“準備的記憶集合中包含引用這個車廂對象的對象集合,如果一個記憶集合爲空,這說明與之對應的那個車廂上的所有對象都沒有外部引用,而這些對象就是可以被垃圾回收器回收的對象。

記憶集合是一個用來提高火車回收算法效率的工具。如果發現一輛”火車“的某個車廂有空的記憶集合,那麼我們就知道這節車廂上全是垃圾,那麼它佔用的空間可以立即被回收。同樣如果一輛”火車“的記憶集合是空的,那麼這輛”火車“也全部是垃圾,那麼它佔用的全部空間也可以立刻被回收。當火車算法將一個對象移動到別的”車廂“活着”火車“上的時候,記憶集合中的信息可以幫助算法快速的更新這些對象的引用到新的內存位置。

儘管火車算法在一次調用中能拷貝的字節量受子堆棧塊的大小的限制,但是移動一個活躍對象(有很對外部引用的對象)的工作量卻是不會受到限制的。每次算法移動一個對象時,它必須遍歷這個對象所在的記憶集合並且更新所有指向這個對象的引用到最新的內存位置。由於一個對象的引用的數量是沒有限制的,因此更新這個對象的引用所需的時間也是沒有限制的。所以在某些情況下火車算法還是會被中斷的。然而,儘管會因爲活躍對象使回收過程被打斷,火車算法在很多時候還是可以很好的工作。

對象終結方法

在java總,對象可能會有一個對象終結器:在垃圾回收器釋放一個對象前必須調用的方法。潛在的終結方法會增加JVM垃圾回收工作的複雜性,因爲垃圾回收器在釋放對象前必須檢查每一個將要被回收的垃圾對象是否擁有finalize()方法。

因爲finalize()方法,垃圾回收器在回收垃圾時必須要多執行幾個步驟的操作。首先,垃圾回收器必須檢測出所有沒有引用的垃圾對象,然後垃圾回收器要檢查這些對象中是否有對象擁有finalize()方法,如果有足夠的時間,在這個時候(對象被釋放前)要爲那些擁有finalize方法的對象調用該方法。

在執行完所有對象的finalize方法後,垃圾回收器有必須從跟引用集合開始檢測沒有被引用的對象,爲什麼需要這個工作呢?那是因爲對象的finalize方法可能會使對象復活(使對象再次被其他對象引用),最後垃圾回收器纔可以釋放它檢測出來的垃圾對象。

爲了減少回收所消耗的時間,垃圾回收器可以有選擇的在檢測垃圾對象和執行垃圾對象的finalize方法中間插入一箇中間步驟:從等帶被執行finalize方法的對象開始檢測是否有對象會復活,任何一個從根節點不可達的且在finalize方法中無法復活的對象都是可以被立即回收的對象。

如果一個有finalize方法的對象沒有被外部引用,並且它的finalize方法已經執行過了,那麼垃圾回收器就必須保證它的finalize方法不會被再次執行。如果一個對象在自己的或者其他對象的finalize方法執行中被複活了,然後又變成了未被引用的,那麼垃圾回收器必須像對待沒有finalize方法的對象一樣對待它。

在你寫java程序的時候,你必須時刻記住運行對象finalize放的是垃圾回收器,因爲我們無法準確的預測一個垃圾對象會在何時被垃圾回收器回收,所以我們也就無法預測對象的finalize方法何時會被執行。你應該避免寫出正確性依賴於對象finalize方法的代碼。例如一個垃圾對象A的finalize方法釋放了一個資源,而這個資源在後面又是程序需要的。這個資源本在A的finalize方法執行完成之前變成了不可達的。假如程序在垃圾回收器運行A的finalize方法之前需要這個資源的話,那麼程序就可能因爲這個資源仍被別的對象佔用而出現問題。

對象的可達生命週期

在(JVM)1.2版本以前,從垃圾回收器的角度來看,每一個堆棧上的對象都處於三種狀態中的一種:可達的,可復活的,不可達的。如果垃圾回收器可以從根引用集合跟蹤到某個對象那麼這個對象就是可達的。每一個對象的生命期都是從可達狀態開始的,這個狀態一直維持到沒有任何外部引用爲止。當垃圾回收器釋放對了某個對象的所有引用,這個對象就變成了可以復活的。


如果垃圾回收器暫時無法從根引用集合跟蹤到某個對象,但是後面這個對象又可能因爲它的finalize方法被運行而變得可達,那麼這個對象就是可復活的,所有的對象都會經歷可復活狀態期,而不僅僅是那些聲明瞭finalize方法的對象。在之前已經提到,一個對象的finalize方法可能是它自己復活,也可能使得其他的對象復活,所以垃圾回收器在確定一個對象不可能被複活之前不可以回收處於可復活狀態的對象佔用的內存空間。通過運行對象的finalize方法,垃圾回收器會轉變所有可復活狀態的對象,可能從可復活變爲不可達,也可能從可復活變爲可達。


如果一個對象處於不可達狀態,那麼這個對象不可能再次被程序用到也不可能因爲它自己或者別的對象的finalize方法的調用而復活,以後它不會對運行的程序產生任何影響,因此垃圾回收器可以毫無顧忌的回收它佔用的內存。

在1.2版本的jvm中,這三種原始的狀態----可達,可復活,不可達------變成了三種新的狀態softly可達,weakly可達,phantom可達,這三種新的狀態相對於1.2版本之前的strongly可達都表示對象某種程度上的可達性,任何直接直接被根引用集合引用的對象都是strongly可達的,例如局部變量,同樣任何一個被strongly可達的對象引用的對象也是strongly可達的。


引用對象

弱(weakly)可達形式的對象都和之前介紹的1.2版本中的“引用對象”有關,一個“引用對象“封裝了一個指向其他對象的引用,被稱爲引用者。所有的”引用對象“都是java抽象類java.lang.ref.Reference的子類的一個實例。Reference類家族包含三個子類:SoftReference,WeakReference和PhantomReference如下圖所示。一個SoftReference對象封裝了一個指向引用者的”軟引用(soft reference)“,一個WeakReference對象封裝了一個指向引用者的”弱引用(weak reference)“,一個PhantomReference對象封裝了一個指向引用者的”虛位引用(phantom reference)“。強引用與他的三個減弱的引用兄弟之間的根本區別在於強引用會阻止垃圾回收器回收引用者,而其他三個不會阻止垃圾回收器這麼做。


如果要創建soft、weak活着phantom引用你只需要將一個強引用作爲參數傳遞給這三個對象的合適的構造器就可以了。例如創建一個指向Cow對象的軟引用,你就給SoftReference構造器傳遞一個指向Cow對象的強引用就可以了。你維持一個指向SoftReference對象的強引用的同時也就維持了一個指向Cow對象的軟引用。下圖就展示了一個封裝有Cow對象軟引用的軟引用對象。



圖示中,SoftReference對象被一個局部變量強引用,這個局部變量就像其他局部變量一樣被作爲跟引用集合的一個對象節點,之前已經說過根引用集合中的對象和被強引用對象引用的對象都是強引用對象。由於圖中的SoftReference對象被一個強引用對象引用,所以SoftReference對象也是Strongly可達的。假如SoftReference對象只包含了指向Cow對象的一個引用,那麼Cow對象就是弱引用的。這是因爲垃圾回收器只能通過一個弱引用找到這個Cow對象。

一旦一個”引用對象“被創建,它就會繼續持有引用者的軟引用,弱引用和虛位引用直到它被程序活着垃圾回收器清除。要清除一個引用對象程序和垃圾回收器只需要調用它的clear方法就可以了。清除掉引用對象會使得它持有的軟引用,弱應用和虛位引用全部失效。例如如果一個程序或者垃圾回收器調用了圖示中的SoftReference對象的clear方法,那麼指向Cow對象的軟引用就會失效,那麼Cow對象就再也不是softly可達的了。


可達狀態變換

就像之前提到的引用對象的作用是讓你持有可被來及回收器自由回收的對象的引用,,換一種說法就是垃圾回收器可以修改任何非strongly引用的任何對象的狀態。因爲當你持有soft,weak,phantom引用的的很多時候垃圾回收器跟蹤對象的可達性狀態變換是很重要的,爲了跟蹤對象的可達性狀態,你可以將引用對象和引用隊列關聯起來。引用隊列是一個java.lang.ref.ReferenceQueue實例,在當對象的狀態改變時來及回收器會將對應的對象入隊,通過設置監控隊列,當對象的可達性狀態變化的時候你就可以收到垃圾回收器異步發送的通知。

要將一個引用對象和引用隊列關聯起來,你只需要將一個對象的引用傳遞個引用隊列的構造方法,這樣一個引用對象會被創建,它會持有一個指向引用者的引用,並且持有一個指向引用隊列的引用。當垃圾回收器更愛一個相關的引用者的可達性狀態的時候它就會將引用對象加到與對象關聯的引用隊列中,例如下圖:當一個WeakReference對象被創建,兩個引用:一個指向Fox對象的引用和一個指向ReferenceQueue對象的引用會被被傳遞給構造方法。當垃圾回收器決定回收那個weakly可達的Fox對象的時候,它會將WeakReference對象添加到隊裏中的同時活着稍後清除Weakreference對象。


垃圾回收器調用Reference超類的enqueue()方法將引用對象添加到與其關聯的隊列的末尾,enqueue()方法只有在對象滿足以下條件時纔將對象入隊:引用對象是與隊列想關聯的;第一次在這個對象上調用enqueue方法。程序可以通過兩種方法監控對象的引用,1:調用poll()方法進行輪詢,2:調用remove方法進行阻塞。如果一個引用對象在隊列調用poll活着remove方法的時候再隊列中是處於等待狀態,方法將會把對象從隊列中移除並返回移除的對象。如果隊列中沒有對象,poll方法會直接返回null,而remove方法將會阻塞直到下一個引用對象入隊。一旦有一個對象進入隊列,remove方法就會將它移除並返回它。

垃圾回收器在不同的情況下入隊soft,weak和phantom對象來表示三種不同的對象可達性狀態變換。以下是對在六種不同的可達性狀態和場景下狀態變換的詳細說明:

1:strongly可達:一個對象可以從根引用集合開始被任何對象跟蹤到。一個對象的生命週期是從strongly可達開始的,只要從根引用集合可以跟蹤到這個對象或者對象被別的strongly引用的對象引用,那麼這個對象就會保持strongly引用狀態。垃圾回收器也不會回收被strongly引用對象佔用的空間。

2:softly可達:如果一個對象不是strongly可達的,但是可以通過根引用集合的一個或者多個soft引用對象找到它,那麼這個對象就是softly可達的。垃圾回收器可能會回收這樣的對象佔用的空間,如果垃圾回收器將這個對象佔用的空間回收,那麼所有指向這個對象的soft引用都會被清除。當垃圾回收器清除一個與引用隊列關聯的soft引用對象的時候,垃圾回收器會將它入隊。

3:weakly可達:一個既不是strongly可達也不是softly可達的對象,如果可以通過根引用集合的一個或者多個(沒有被清除的)weak引用對象找到它,那麼它就是weakly可達的。垃圾回收器必須回收被這種weakly引用的對象佔用的內存空間。回收的時候,垃圾回收器會清除所有指向這個weakly引用對象的引用。當垃圾回收器清除某個與引用隊列關聯的weak引用對象的時候,它會將這個對象入隊。

4:可復活的:一個對象不是strongly,softly,weakly可達,但是仍可能因爲某些對象的finalize方法的執行變回以上三種狀態,那麼這個對象就是可復活的。

5:phantom可達:一個對象不是strongly,softly,weakly可達,經檢測它也不可能被任何finalize方法復活,並且通過根引用集合的一個或者多個(未被清除的)phantom(虛位)引用對象找到,那麼這個對象就是phantom可達的。一旦一個被虛位引用對象引用的對象變成了phantom可達的,垃圾回收器就會將它入隊,垃圾回收器永遠不會清除phantom引用。所有的虛位引用都必須被程序顯示的清除。

6:不可達:不處於以上5種狀態的對象就是不可達狀態的對象,這樣的對象佔用的內存空間可以被垃圾回收器回收。


注意垃圾回收器在softly引用對象,weakly引用對象的引用者離開相關的可達性狀態的時候纔將它們入隊,而對於phantom引用來說,是在引用者進入相關的狀態時垃圾回收器纔會將它們入隊。你也可看出垃圾回收器在清除soft和weak引用對象是是在它們入隊之前,而清除phantom引用對象是則不是這樣處理的。因此,垃圾回收器在將soft引用對象入隊時表示soft引用對象的引用者們剛剛離開softly可達狀態,同樣垃圾回收器入隊weakly引用對象時表示它們的引用者們剛剛離開weakly可達狀態。但是垃圾回收器入隊phantom引用對象則是表示它們的引用這門剛剛進入phantom可達狀態。phantom引用對象會一直保持這種狀態直到它們的引用對象顯示的被程序刪除。


緩存,Canonicalizing映射和Pre-Mortem清理

垃圾回收器區別對待soft,weak和phantom對象的原因是這三種對象是爲程序提供不同的服務而生的。soft引用使你能夠創建內存內的緩存,這種緩存對整個程序所需要的內存空間的大小非常敏感。weak引用使你能夠創建canonicalizing映射,比如哈希表,這種哈希表的鍵和值在程序不再引用他們的時候會被刪除。phantom引用是你能夠建立比finalize方法更靈活的pre-mortem清理策略。

如果要使用soft或者weak引用對象的引用者,你就需要在引用對象上調用get()方法,如果引用尚未被清理,那麼get()方法會使你得到一個指向引用者的強引用(strong reference),這個時候你就可以以某種特殊的方式使用它。如果對象已經被清理掉了,get()返回null。如果你在一個phantom引用對象上調用get方法,它會一直返回null,即使這個引用對象還沒有被垃圾回收器清理掉。因爲phantom可達狀態是在一個對象已經不可能被複活後纔會出現的狀態。phantom一樣對象無法提供訪問它的引用者的方法。因此如果一個對象到達phantom引用狀態,那麼它一定是無法被複活的。


虛擬機的實現要求在拋出OutOfMemoryError異常之前對soft引用進行清理,如果不會拋出此異常虛擬機何時清理soft引用使不確定的。然而,我們鼓勵的虛擬機實現方式是:(1)在程序需要的內存空間的大小超過了虛擬機提供的內存空間的大小的時候纔去清理soft引用;(2)在清理新的soft引用之前清理較老的soft引用;(3)先清理長時間未被使用過的soft引用,然後清理近期被使用過的soft引用。

soft引用使你能夠將那些可以以較慢的速度從外部數據源獲得的數據存儲在內存緩存中,這樣的外部包括文件,數據庫和網絡等。只要虛擬機擁有足夠的內存存儲堆棧上的所有strong引用和soft引用,soft引用通常就可以“足夠強的”持有被soft引用引用的堆棧數據。然而如果內存變得很稀有,垃圾回收期就可能會決定去清理soft引用,回收它們佔用的空間。下一次程序要是用那些數據的時候,就需要從外部的數據源去重新加載。


weak引用和soft引用很類似,有一點不同的是:垃圾回收器可以自由的決定何時釋放soft引用,但是必須在檢測到weak引用時儘快將weak引用釋放掉。weak一樣讓你能夠創建canonicalizing映射。java.util.WeakHashMap類使用weak引用提供canonicalizing映射。你可通過put方法向WeakHashMap實例中放入鍵值對,這和其他實現了java.util.Map的類的操作一樣。但是在WeakHashMap內部,key對象是通過一個與引用隊列關聯的weak引用存儲的,如果垃圾回收器檢測到一個key對象是weakly可達狀態,它就會清理任何引用key對象的弱引用對象並將它們入隊。下一次訪問WeakHashMap的時候,它就會輪詢隊列並提取出之前垃圾回收器放入隊的對象。WeakHashMap然後就會從它的映射關係中移除任何key對象在隊裏中出現過的鍵值對。因此如果你添加一個鍵值對到WeakHashMap中,它就會一直存在於WeakHashMap中,直到程序顯示的使用remove方法將它移除並且垃圾回收器也沒有認爲key對象是一個處於weakly可達狀態的對象。


phantom可達表示一個對象已經準備好被回收了。當垃圾回收器認爲一個phantom引用的引用者對象是phantom可達的,它就將這個phantom引用對象添加到一個關聯的隊列中(和soft,weak可以選擇性的創建關聯隊列對象不同,phantom引用創建時必須有一個關聯的隊列)你可以使用phantom對象到達這個事件去觸發一些你希望在對象生命終結時執行的操作。因爲你無法獲得一個指向phantom對象的strong引用(調用它的get方法只會返回null),你就無法採取那些要求你訪問目標實例變量的操作。一旦你的phantom引用的pre-mortem清理操作已經完成,你就必須在phantom引用對象上調用clear方法,這將使phantom引用對象的引用者從phantom可達狀態轉換爲不可達狀態。


關於三種弱引用大家可以參考:http://blog.csdn.net/kx_nullpointer/article/details/8291936

未完,待續。。。。。

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