java虛擬機器總共分為五個地區,其中三個是線程私人:程式計數器,虛擬機器棧,本地方法棧,兩個是線程共用:堆,方法區。線程私人的地區等到線程結束時(棧幀出棧時)會自動被釋放,空間比較容易清理。而線程共用的java堆和方法區中的空間較大而且沒有線程的回收容易產生很多垃圾資訊,GC記憶體回收真正關心的就是這部分。
java堆和方法區主要存放各種類型的對象(方法區中也儲存一些靜態變數和全域常量等資訊),那麼我們在使用GC對其進行回收的時候首先要考慮的就是如何判斷一個對象是否應該被回收。也就是要判斷一個對象是否還有其他的引用或關聯使得這個對象處於存活的狀態。我們需要將不在存活狀態的所有對象標記出,以便於GC進行回收。
判斷對象是否存活有兩種比較常見的方法:引用計數法與可達性分析演算法。 引用計數法
引用計數法的邏輯非常簡單,但是存在問題,java並不採用這種方式進行對象存活判斷。
引用計數法的邏輯是:在堆中儲存物件時,在對象頭處維護一個counter計數器,如果一個對象增加了一個引用與之相連,則將counter++。如果一個參考關聯性失效則counter–。如果一個對象的counter變為0,則說明該對象已經被廢棄,不處於存活狀態。
這種方法來標記對象的狀態會存在很多問題:
1 jdk從1.2開始增加了多種引用方式:軟引用、弱引用、虛引用,且在不同引用情況下程式應進行不同的操作。如果我們只採用一個引用計數法來計數無法準確的區分這麼多種引用的情況。
引用計數法無法解決多種類型引用的問題。但這並不是致命的,因為我們可以通過增加邏輯區分四種引用情況,雖然麻煩一些但還算是引用計數法的變體,真正讓引用計數法徹底報廢的下面的情況。
2 如果一個對象A持有對象B,而對象B也持有一個對象A,那發生了類似作業系統中死結的迴圈持有,這種情況下A與B的counter恒大於1,會使得GC永遠無法回收這兩個對象。 可達性分析演算法
在主流的商用程式語言中(Java和C#),都是使用可達性分析演算法判斷對象是否存活的。這個演算法的基本思路就是通過一系列名為GC Roots的對象作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是停用,下圖對象object5, object6, object7雖然有互相判斷,但它們到GC Roots是不可達的,所以它們將會判定為是可回收對象。
那麼那些點可以作為GC Roots呢。一般來說,如下情況的對象可以作為GC Roots: 虛擬機器棧(棧楨中的本地變數表)中的引用的對象 方法區中的類靜態屬性引用的對象 方法區中的常量引用的對象 本地方法棧中JNI(Native方法)的引用的對象 HotSpot虛擬機器如何?可達性演算法。
java中的主流虛擬機器HotSpot採用可達性分析演算法來確定一個對象的狀態,那麼HotSpot在具體實現該演算法時採用了哪些結構。
使用OopMap記錄並枚舉根節點
HotSpot首先需要枚舉所有的GC Roots根節點,虛擬機器棧的空間不大,遍曆一次的時間或許可以接受,但是方法區的空間很可能就有數百兆,遍曆一次需要很久。更加關鍵的是,當我們遍曆所有GC Roots根節點時,我們需要暫停所有使用者線程,因為我們需要一個此時此刻的”虛擬機器快照”,如果我們不暫停使用者線程,那麼虛擬機器仍處於運行狀態,我們無法確保能夠正確遍曆所有的根節點。所以此時的時間開銷過大更是我們不能接受的。
基於這種情況,HotSpot實現了一種叫做OopMap的資料結構,這種資料結構在類載入完成時把對象內的位移量是什麼類型計算出,並且存放下位置,當需要遍曆根結點時訪問所有OopMap即可。
用安全點Safepoint約束根節點
如果將每個符合GC Roots條件的對象都存放進入OopMap中,那麼OopMap也會變得很大,而且其中很多個物件很可能會發生一些變化,這些變化使得維護這個映射表很困難。實際上,HotSpot並沒有為每一個對象都建立OopMap,只在特定的位置上建立了這些資訊,這些位置稱為安全點(Safepoints)。
為了保證虛擬機器中安全點的個數不算太多也不是太少,主要決定安全點是否被建立的因素是時間。當進行了耗時的操作時,比如方法調用、迴圈跳轉等時會產生安全點。此外,HotSpot虛擬機器在安全點的基礎上還增加了安全區域的概念,安全區域是安全點的擴充。在一段安全區域中能夠實現安全點不能達成的效果。