JVM學習-GC之追蹤式垃圾收集算法基礎

  學習JVM的垃圾回收,離不開的是追蹤式垃圾回收算法,現有的主流Java虛擬機都採用的是追蹤式回收算法。對比於引用計數式垃圾收集,追蹤式垃圾回收算法都是採用的間接式的回收策略,也就是這種策略並不是直接尋找垃圾自己,而是先尋找哪些對象存活,而後反過來判斷其他全部的對象爲垃圾對象。追蹤式回收算法自己包括標記-清除(Mark-Sweep)、標記-複製(Mark-Copy)、標記-整理(Mark-Compact)這三種回收策略,在真正的回收器上,必定是根據對象的不一樣狀況進行分區或者分代,針對不一樣的區域採起不一樣的回收策略,針對這些狀況,有許多的相關基礎概念展示出來。算法

追蹤回收算法回收策略

追蹤式回收算法自己包括標記-清除(Mark-Sweep)、標記-複製(Mark-Copy)、標記-整理(Mark-Compact)這三種回收策略,這三種策略都有一個共通之處,那就是標記(Mark),關於這個標記的過程在上一篇文章(JVM學習-GC之判斷對象存活)有講過,就是裏面的可達性分析算法,本篇文章將再也不作多餘概述。編程

標記-清除(Mark-Sweep)算法

標記-清除(Mark-Sweep)算法是一種典型的非移動式回收算法,是全部追蹤式回收算法的基礎,其餘的算法都是針對標記-清除(Mark-Sweep)算法的缺點改進而來。數組

原理

  在標記過程完成以後,將未標記的對象進行回收。bash

優缺點

  優勢:
    1. 標記-清除(Mark-Sweep)算法的吞吐量(吞吐量就是處理器用於運行用戶代碼的時間與處理器總消耗時間的比值,吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 運行垃圾收集時間))較高;
    2. 標記-清除(Mark-Sweep)算法對空間的利用率比較高,既不須要像標記-複製(Mark-Copy)算法劃出多餘的空間來進行復制對象,也不須要像引用計數算法爲每一個對象設置引用計數器;
  缺點:
    1. 標記-清除(Mark-Sweep)算法的執行效率不太穩定;
    2. 標記-清除(Mark-Sweep)算法在清除的過程當中會產生大量的碎片化空間,空間的碎片化太多,會致使程序在運行時分配對象的時候,沒法找到足夠大的連續空間,致使提早進行另外一次垃圾收集;數據結構

標記-複製(Mark-Copy)算法

標記-複製(Mark-Copy)算法簡稱爲複製算法,爲了解決標記-清除(Mark-Sweep)算法面對大量可回收對象時,執行效率低的問題。併發

半區複製原理

  基本的複製回收器會將堆劃分稱爲兩個大小相等的半區,分別是來源空間和目標空間。每次在程序運行時,只用其中的來源空間來進行對象的內存分配,當來源空間的內存不足時,進行垃圾回收,交換兩個半區的角色,而後將存活的對象移到另外一個半區的一端,最後將垃圾回收的半區內存清零。編程語言

半區複製優缺點

  優勢:
    1. 半區複製提高了垃圾回收的效率;
    2. 半區複製減小了碎片化空間的誕生;
  缺點:
    1. 半區複製將原來的可用內存減小了一半;post

Appel式回收原理

  Appel式回收是針對標準ML提出的一種自適應分代策略,在ML語言中,一次回收完成一般只有不到2%的對象可以存活,Appel式回收正式針對這一種狀況而設計的策略。Appel式回收策略將空間分爲三個:老年代、複製保留區、新生代,在HotSpot虛擬機中的實現中新生代收集器將新生代變成Eden空間,將複製保留區變成兩塊較小的Survivor空間,在程序運行中每次分配內存只使用Eden和其中一塊Survivor空間,在發生垃圾收集時,將存活的對象複製到保留的那一塊Survivor上,另外兩塊空間直接清零(在HotSpot虛擬機中Eden和Survivor的比例爲8:1)。
  當Survivor空間不足以容納一次Minor GC以後,就須要依賴其餘內存區域(大部分時候是老年代)進行分配擔保,這些沒有足夠空間存放的對象直接進入其餘區域。性能

Appel式回收優缺點

  優勢:
    1. Appel式回收能夠爲複製保留區的大小進行動態調節;
  缺點:
    1. 必需要有其餘空間進行空間擔保;學習

標記-整理(Mark-Compact)算法

  在標記-清除(Mark-Sweep)算法這種非移動式回收算法中最大的問題就是會產生碎片化的空間,而標記-整理(Mark-Compact)算法正是爲了下降內存碎片化提出來的解決策略。

原理

  在標記以後,把全部標記的對象都移到內存空間的一端,而後直接把邊界以外的內存清零。

優缺點

  優勢:
    1. 減小了內存碎片化;
    1. 在對象大量存活的狀況下,效率要高於複製算法;
  缺點:
    1. 整理(Compact)過程繁瑣,在大多數算法下須要屢次遍歷內存,STW(Stop The World)時間比清理(Sweep)時間長;

關於標記-整理(Mark-Compact)算法吞吐量的我的理解:在算法上標記-整理(Mark-Compact)算法通常須要屢次遍歷堆的過程,因此標記-整理(Mark-Compact)算法上的吞吐量是不如標記-清除(Mark-Sweep)算法的吞吐量的,可是對於整個系統來講標記-清除(Mark-Sweep)算法是屬於不移動回收算法,不移動回收算法有一個明顯的缺點就是在分配內存時更加複雜,對於大量的碎片化空間就只能經過其餘分配空間手段(好比:分區空閒分配鏈表)來解決,而分配內存是程序運行過程當中最頻繁的操做,因此在系統的吞吐量上標記-整理(Mark-Compact)算法上的吞吐量是優於標記-清除(Mark-Sweep)算法的吞吐量的。

三色算法

在標記過程當中,三色算法是全部賦值器和回收器遵照的不變式。

三色算法原理

  在遍歷對象圖的過程當中,回收器把是否訪問過的對象根據「是否訪問過」來把全部對象標記成三種顏色。

  • 白色:對象還沒有被回收器訪問到,在回收週期的開始階段,全部的對象都是白色,在回收週期結束的時候,全部白色對象都是不可達對象。
  • 灰色:表示對象已經被回收器掃描過,可是對象上還有一個或者多個引用沒有進行掃描。
  • 黑色:對象已經被回收器掃描過,而且全部的引用已經被掃描。黑色對象永遠不可能直接指向白色對象,也永遠不會被回收器再次掃描,除非顏色變化。

  回收器從白色根節點出發,逐步把對象圖的對象變成灰色再到黑色,當所有遍歷完成後全部可達的節點變成黑色,不可達的節點依舊是白色。

對象丟失問題

  當在同時產生下面兩個條件時,會產生對象丟失問題

  • 賦值器插入了一條或者多條從黑色對象到白色對象的新引用
  • 賦值器刪除了所有從灰色對象到該白色對象的直接或者間接引用

弱三色不變式和強三色不變式

弱三色不變式和強三色不變式是爲了解決對象丟失的。

  • 弱三色不變式:全部被黑色引用的白色對象都會被灰色保護(灰色保護:白色對象直接或間接被灰色對象引用)。弱三色不變式打破了對象丟失條件的條件二。
  • 強三色不變式:不存在黑色對象指向白色對象的指針。強三色不變式打破了對象丟失條件的條件一。

併發時三色問題的解決方案

增量更新

  增量更新要破壞的是第一個條件,當黑色對象插入新的指向白色對象的引用關係的時候,就將這個新插入的引用記錄下來,等併發掃描結束以後,再將這些記錄過的引用關係中的黑色對象爲根,從新掃描一次。

原始快照(SATB)

  原始快照(SATB)要破壞第二個條件,當灰色對象要刪除指向白色對象的引用關係的時候,就要將這個要刪除的引用記錄下來,在併發掃描結束以後,再將這些引用記錄過的引用關係中的灰色對象爲根,從新掃描一次。

GC的實現方式

在追蹤式垃圾收集算法裏面最早開始的就是用可達性分析算法標記(Mark)對象,在可達性分析算法裏面第一步就是肯定根節點集合(Root Set),而如何肯定根節點集合(Root Set),就影響了GC的實現方式。

保守式GC

  若是JVM選擇不記錄內存中每一個數據的類型,那麼JVM就沒法區份內存裏某個位置上的數據到底應該解讀爲引用類型仍是其餘數據類型。這種條件下,實現出來的GC就會是「保守式GC(Conservative GC)」。在進行GC的時候,JVM開始從一些已知位置(例如說JVM棧)開始掃描內存,掃描的時候每看到一個數字就看看它「像不像是一個指向GC堆中的指針」,判斷的方式相似於上下邊界的檢查,對其檢查等。

半保守式GC

  JVM能夠選擇在棧上不記錄類型信息,而在對象上記錄類型信息。這樣的話,掃描棧的時候仍然會和保守式GC(Conservative GC)的過程同樣,但掃描到GC堆內的對象時由於對象帶有足夠類型信息了,JVM就可以準確判斷出在該對象內什麼位置的數據是引用類型了。這種是「半保守式GC」,也稱爲「根上保守(ConserVative With Respect To The Roots)」。

準確式GC

  「準確式GC」所謂的準確,關鍵就是「類型」,也就是說給定某個位置上的某塊數據,要能知道它的準確類型是什麼,這樣才能夠合理地解讀數據的含義;GC所關心的含義就是「這塊數據是否是指針」,這塊數據不只僅是GC堆內對象上的數據,包括活動記錄(棧+寄存器)裏的數據

分代收集理論相關

追蹤式垃圾收集算法在少許垃圾回收的時候效率很是高效,特別是複製回收算法。可是長壽對象的存在會影響到回收回收的效率,這個時候就經過分區,使長壽的數據都堆積在一邊,這樣對年輕的數據使用複製回收算法就能夠大大提高效率。在真實的商用垃圾回收大部分都採用了分代理論,哪怕G1回收器做爲全區域回收器,在區域裏面依舊用到了分代概念。

分代收集理論

分代收集理論創建在兩個分代假說之上:

  • 弱分代假說:大多數對象都是朝生夕滅的。這個假說已經在不一樣的編程語言和編程範式中獲得證明;
  • 強分代假說:越長壽的對象越不容易死亡。這個假說證據稍顯不足,可是卻依舊給大對象回收處理有必定的意義。

  這兩個假說共同奠基了多款經常使用的垃圾收集器的一致設計原則:收集器應該將堆劃分出不一樣區域,而後將回收對象依據年齡分配到不一樣的區域之中。

  除此以外新生代對象徹底能夠由老年代對象引用,若是產生這種引用,就須要遍歷整個老年代來肯定可達性分析的準確性,這樣對內存回收帶來極大的性能負擔,因此引出了另外一條假說

  • 跨代引用假說:跨代引用對比與同代引用來講僅佔極少數

記憶集(Remembered Set)和卡表(Card Table)

  對於跨代引用來講,目前解決的方式就是記憶集和卡表。記憶集(Remembered Set)是一種抽象概念,而卡表(Card Table)能夠是記憶集(Remembered Set)的一種實現方式。
  記憶集(Remembered Set)是在實現部分垃圾收集(partial GC)時用於記錄從非收集部分指向收集部分的指針的集合的抽象數據結構
  1. 記錄精度(其實不管是remembered set仍是card table,記錄精度都有很大的選擇餘地):

  • 字粒度:每一個記錄精確到一個機器字(Word)。該字包含有跨代指針。
  • 對象粒度:每一個記錄精確到一個對象。該對象裏有字段含有跨代指針。
  • 卡(card)粒度:每一個記錄精確到一大塊內存區域。該區域內有對象含有跨代指針。

  (還有許多類型的顆粒度,能夠本身想象)

  2. 數據結構
  記憶集(Remembered Set):使用指針(對象指針或者字指針)的數據來實現,例如

struct RememberedSet {  
  Object* data[MAX_REMEMBEREDSET_SIZE];  
}; 
複製代碼

卡表(Card Table):使用字節數組來實現卡(card)的記錄,每一個卡(card)對應該數組裏的一個bit或一個byte,例如

struct CardTable {  
  byte table[MAX_CARDTABLE_SIZE];  
};  
複製代碼

字節數組卡表的每個元素都對應着其標識的內存區域中一塊特定大小的內存塊,這個內存塊被稱做「卡頁」(Card Page),通常來講卡頁大小都是以2的N次冪的字節數。一個卡頁的內存中一般不止包含一個對象,只要卡頁中有一個或者多個對象的字段存在着跨代指針,那就將對應的卡表的數組元素的標識變成1,稱爲這個元素變髒,沒有則標識爲0。在垃圾收集過程當中只要篩選出來變髒的元素,把變髒的元素加入根節點集合(Root Set)一併掃描。

對象的年齡

在JVM的HotSpot虛擬機中,對象的年齡放置在對象頭中,每當對象經歷熬過一次回收,年齡加一,最大15。
對象分配規則和晉升老年代規則:

  1. 對象優先在Eden分配
  2. 大對象直接進入老年代
  3. 長期存活對象進入老年代
  4. 動態對象年齡判斷(虛擬機並非老是要求對象年齡須要達到規定的晉升年齡(MaxTenuringThreshold)才能晉升到老年代的,若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡
  5. 空間分配擔保

本文采用《深刻理解Java虛擬機》和《The Garbage Collection Handbook》以及RednaxelaFX大佬的文章進行參考以及學習

相關文章
相關標籤/搜索