《Understanding the JVM》讀書筆記之二——垃圾回收算法

垃圾收集器工作的第一步就是判斷對象是否還活着,通過垃圾回收算法判斷。

一、引用計數算法
  • 在對象A中添加一個引用計數器,當有一個地方引用A時,計數器+1;當引用失效時,計數器-1,任何時刻計數器數值爲0時,這個對象就不會再被使用了;
  • 引用計數法的實現簡單,判斷效率高。但再主流的java虛擬機中沒有使用此算法,原因是,它無法解決相互循環引用問題。

二、可達性分析算法:(不可達意味着對象死亡的可能性高)
  • 通過一系列GC roots 對象作爲起始點,從GC Roots開始向下搜索,所走過的路徑稱爲 引用鏈,當一個對象到GC Root沒有任何引用鏈時,則證明此對象是不可用的。
  • Java中可以被看作GC Root的對象包括:
    a. 虛擬機棧中引用的對象(棧幀中的局部變量表)
    b. 方法區中類靜態屬性引用的對象
    c. 方法區中常量引用的對象
    d. 本地方法棧中JNI(Native)方法引用的對象。

三、生存還是死亡?(即使是不可達的對象,也不是非死不可)
  1. 查找引用鏈
  • 當對象在可達性分析後發現沒有與GC Root相連的引用鏈,此對象會被標記(第一次),並進行一次篩選。
  2. 第一次篩選
  • 篩選的條件:此對象是否需要執行finalize()方法,當此對象沒有覆蓋finalize()方法或finalize()方法已經被調用過=>沒有必要執行,放入被會收集和;否則==>有必要執行;
  3. 放入隊列
  • 如果判定有必要執行finalize,此對象會先被放到F-Queue隊列中,
  4. Finalizer線程
  • 稍後,虛擬機會自動建立Finalizer線程,對F-Queue中的對象觸發finalize()方法,但finalize()方法並不一定會執行完畢。(原因:如果finalize()方法執行緩慢、或死循環將導致整個隊列等待)
  5. 第二次標記
  • 再稍後,GC將對F-Queue中的對象進行第二次標記(逃脫死亡的最後一次機會),標記原則:如果此對象再finalize()方法中與GC Root引用鏈上的任何對象建立關聯==>移出”被回收對象集合“;否則==>不移出。
  6. GC回收

四、方法區是否有必要回收
  • 相比之下,新生代中GC回收效率較高,常規應用一次GC可以回收70~90%的空間;
  • 永久代(方法區)回收性價比較低,主要回收:廢棄常量、無用的類兩種。而在jdk1.7以後,常量池已經移至堆內存,這部分也不應該計算在內。
  • 無用的類需要滿足以下三個條件:
    a. 該類所有實例都被回收
    b. 加載該類的ClassLoader已經被回收
    c. 該類對應的java.lang.Class對象沒有在任何地方引用(沒有反射創建類)

五、常見算法實現
標記-清除算法:
  1. 對所有要回收的對象進行標記(見生存還是死亡)
  2. 回收被標記的對象
  3. 算法問題:
    • 效率:標記和清除效率都不高
    • 空間:清除後會產生大量內存碎片,在之後分配大對象時無法找到足夠的連續內存而提前觸發第二次GC

複製算法:
  1. 將內存容量劃分爲大小相等的兩塊,每次只是用其中的一塊,
  2. 當一塊內存使用滿,將還存活着的對象複製到另一塊內存,
  3. 一次性清除已滿的內存塊。
  4. 算法問題:
    • 對整個區域進行回收,不需要考慮內存碎片問題,而且指針按順序移動,效率高。
    • 但代價是可用內存減少到一半
    • 在對象存活率較高的情況下需要進行較多的複製操作,效率會變低
  5. 現代商業虛擬機都採用這種算法回收新生代,但比例不是1:1,因爲新生代對象有98%左右朝生夕死:
    • HotSpot虛擬機將新生代內存劃分爲一塊較大的Eden區和兩個較小的Survivor區,E,S比例爲8:2。
    • 每次使用其中的Eden區和一個Survivor區,當GC回收時,將所有存活的對象複製到另一個Suivivor區中,這樣只有10%的內存被“浪費”。
    • 而當另外的Survivor區內存不夠分配時,需要依賴其他內存進行擔保(老年代)

標記-整理算法:
  1. 標記過程同 標記-清除
  2. 標記後讓所有對象都向一端移動,然後直接清理掉邊界以外的內存
  3. 主要用於老年代的回收

分代收集算法:
  • 當前商業虛擬機都使用分代收集:將所有對象按照生命週期,將內存劃分爲幾塊,一般劃分爲:新生代和老年代。
  • 對於新生代,每次GC都會回收大量對象,使用複製算法
  • 對於老年代,對象存活率較高,不需使用標記-清除或標記-整理算法來回收

六、HotSpot虛擬機算法實現:
1. 枚舉根節點
  • 在可達性分析中,GC Root節點主要在全局引用和執行上下文中,在大型應用中(方法區幾百兆)必然耗費很多時間,另外,爲了確保一致性,在可達性分析的過程中,整個系統必須停頓(stop the world),即使在CMS收集器(號稱不會停頓)也必然產生停頓。
  • 大部分Java虛擬機都是用準確式GC,虛擬機能夠知道那些地方存儲着對象引用(HotSpot使用OopMap數據結構來實現),防止全局掃描。
  • 在HotSpot中,類加載時就把對象什麼偏移量上對應的數據類型計算出來;在編譯過程中,也會在特定位置記錄下棧和寄存器中哪些位置是引用,這樣GC掃描時就可以直接得知。
2. 安全點
  • 特定位置稱爲安全點==>程序執行時只有在到達安全點時才能開始GC。這就需要考慮兩個問題:
  • Safepoint即不能太少也不能太多,太少會讓GC等待時間過長,太多會增大運行時的負荷。
  • 如何在GC發生時,讓所有線程都停在最近的安全點上。主要有搶先式中斷和主動式中斷兩種方式。
  • 搶先式中斷==>在GC時首先把所有線程強制中斷,如果發現有沒到達安全點的線程,就恢復該線程,讓它“跑”到安全點上。現在幾乎沒有虛擬機使用
  • 主動式中斷==>當需要GC時,不主動中斷線程,只設置一個標誌,各個線程主動輪詢這個標誌,發現中斷標誌爲真時,線程自己中斷掛起。而輪詢標誌的地方是安全點重合+創建對象需要分配內存的地方
3. 安全區域
  • 爲了應對線程不執行的時候(沒有搶佔到CPU時間或線程sleep),無法響應線程中斷的請求,線程也就無法暫停的問題,引入了安全區。
  • 安全區是在一段代碼片段中,引用關係不會發生變化,在這個區域中的任意地方開始GC都是安全的。
  • 在GC時不用管自己標識爲SafeRegion的線程了,在線程要離開SafeRegion時,需要檢查系統是否已經完成了根節點枚舉,完成則繼續執行,否則等待收到安全離開SafeRegion的信號爲止。

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