jvm學習-垃圾回收的一些知識點

部分圖片和描述來自參考資料 ,非原創

對象回收處理過程

img

如何標定對象是否存活

兩種方法 :

  • 引用計數方法
  • 可達性分析算法

引用計數方法

就和 ReentrantLock 可重入鎖一樣 ,內部維繫着一個 state , 當同個線程重入結束後就會歸零 , 但是這種方法有點問題

public static void testGC() {
    ReferenceCountingGC objA = new ReferenceCountingGC();
    ReferenceCountingGC objB = new ReferenceCountingGC();
    objA.instance = objB;
    objB.instance = objA;
    objA = null;
    objB = null;
    // 假設在這行發生GC,objA和objB是否能被回收?
    System.gc();
}

兩個對象互相引用使得引用計數大於0 , 但是又沒有給使用到.

可達性分析算法

就是先確定一個 GC-Root , 然後往下搜索, 途徑的對象就進行染色 ,沒有被染色的肯定就是應該回收的對象.

img

在Java技術體系裏面,固定可作爲GC Roots的對象包括以下幾種:

  • 在虛擬機棧(棧幀中的本地變量表)中引用的對象,譬如各個線程被調用的方法堆棧中使用到的參數、局部變量、臨時變量等。
  • 在方法區中類靜態屬性引用的對象,譬如Java類的引用類型靜態變量。
  • 在方法區中常量引用的對象,譬如字符串常量池(String Table)裏的引用。
  • 在本地方法棧中JNI(即通常所說的Native方法)引用的對象。
  • Java虛擬機內部的引用,如基本數據類型對應的Class對象,一些常駐的異常對象(比如NullPointExcepiton、OutOfMemoryError)等,還有系統類加載器。
  • 所有被同步鎖(synchronized關鍵字)持有的對象。
  • 反映Java虛擬機內部情況的JMXBean、JVMTI中註冊的回調、本地代碼緩存等。

img

實現細節知識點

OopMap

OopMap 只存儲與 GC Root 相關的引用信息,而不會存儲所有的引用信息。
定義 : 一旦類加載動作完成的時候,HotSpot就會把對象內什麼偏移量上是什麼類型的數據計算出來,在即時編譯(見第11章)過程中,也會在特定的位置記錄下棧裏和寄存器裏哪些位置是引用。

動機 : 避免了每個內存角落尋找 GC-Root

img

img

oopMap的全稱是Object-oriented Programming Map,它是Java虛擬機中的一種數據結構,用於記錄Java對象中的字段信息和類型信息。(通俗地說就是 內存哪個位置的對象引用到了 GC-Root) , 那麼如果每次創建對象的時候就判斷一下是不是引用到了 GC-Root , 如果是, 那麼就加入到 oopMap 中去 ,這樣的話 ,效率就太慢了, hotspot 是集中在一個時間點才更新 oopmap , 這個時間點就是安全點

安全點 safepoint

每個被JIT編譯過後的方法也會在一些特定的位置記錄下OopMap,記錄了執行到該方法的某條指令的時候,棧上和寄存器裏哪些位置是引用。這樣GC在掃描棧的時候就會查詢這些OopMap就知道哪裏是引用了。這些特定的位置主要在:

**1、循環的末尾 **
**2、方法臨返回前 / 調用方法的call指令後 **
3、可能拋異常的位置

這種位置被稱爲“安全點”(safepoint)。之所以要選擇一些特定的位置來記錄OopMap,是因爲如果對每條指令(的位置)都記錄OopMap的話,這些記錄就會比較大,那麼空間開銷會顯得不值得。選用一些比較關鍵的點來記錄就能有效的縮小需要記錄的數據量,但仍然能達到區分引用的目的。

安全點位置的選取基本上是以“是否具有讓程序長時間執行的特徵”爲標準進行選定的 (原因至今不知道)

img

假如此時JVM 在 t1 時間點 , 進行 GC , 那麼 Thread1 ``Thread2 ``Thread3 此時並沒有到安全點上, JVM 處理這樣的問題會有兩種方式

  • 主動式中斷
  • 搶先式中斷

如果是主動式中斷 :

在垃圾收集發生時,系統首先把所有用戶線程全部中斷,如果發現有用戶線程中斷的地方不在安全點上,就恢復這條線程執行,讓它一會再重新中斷,直到跑到安全點上

如果是搶先式中斷 :

不直接對線程操作,僅僅簡單地設置一個標誌位,各個線程執行過程時會不停地主動去輪詢這個標誌,一旦發現中斷標誌爲真時就自己在最近的安全點上主動中斷掛起。輪詢標誌的地方和安全點是重合的,另外還要加上所有創建對象和其他需要在Java堆上分配內存的地方,這是爲了檢查是否即將要發生垃圾收集,避免沒有足夠內存分配新對象。

毫無疑問, 後者更加消耗性能 , 但是好處就是不會打斷線程 .

安全區域

定義 : 安全區域是指能夠確保在某一段代碼片段之中,引用關係不會發生變化,因此,在這個區域中任意地方開始垃圾收集都是安全的。我們也可以把安全區域看作被擴展拉伸了的安全點。

動機 : 安全點機制保證了程序執行時,在不太長的時間內就會遇到可進入垃圾收集
過程的安全點。但是,程序“不執行”的時候呢?所謂的程序不執行就是沒有分配處理器時間,典型的場景便是用戶線程處於Sleep狀態或者Blocked狀態,這時候線程無法響應虛擬機的中斷請求,不能再走到安全的地方去中斷掛起自己,虛擬機也顯然不可能持續等待線程重新被激活分配處理器時間。對於這種情況,就必須引入安全區域(Safe Region)來解決。

線程在安全區域的工作過程是這樣子的 :

當用戶線程執行到安全區域裏面的代碼時,首先會標識自己已經進入了安全區域,那樣當這段時間裏虛擬機要發起垃圾收集時就不必去管這些已聲明自己在安全區域內的線程了。當線程要離開安全區域時,它要檢查虛擬機是否已經完成了根節點枚舉(或者垃圾收集過程中其他需要暫停用戶線程的階段),如果完成了,那線程就當作沒事發生過,繼續執行;否則它就必須一直等待,直到收到可以離開安全區域的信號爲止。

老生代引用新生代

下面的記憶集卡表 , 寫屏障這些都是爲了解決 "老生代引用新生代" 這個主題 ,記住這一點就好理解了

記憶集與卡表

記憶卡的動機 ,可以參考下面這張圖 :

img

假如我們沒有記憶卡 , 那麼年輕代的偏紅色的元素是否要回收呢 ?

爲了解決跨代引用的問題,JVM 中引入了 Remembered Set。Remembered Set 是一個記錄了跨代引用的數據結構,用於記錄新生代對象引用的老年代對象的地址。在進行 Minor GC 時,JVM 會掃描 Remembered Set 中記錄的地址,將相關的老年代對象標記爲存活對象,從而避免因跨代引用而導致的對象丟失問題。

那麼卡表是什麼 ?

前面定義中提到記憶集其實是一種“抽象”的數據結構,抽象的意思是隻定義了記憶集的行爲意圖,並沒有定義其行爲的具體實現。卡表就是記憶集的一種具體實現,它定義了記憶集的記錄精度、與堆內存的映射關係等。關於卡表與記憶集的關係,讀者不妨按照Java語言中HashMap與Map的關係來類比理解。

也就是說一個是思想 ,一個是具體的實現罷了 . 卡表的每個記錄精確到一塊內存區域,該區域內有對象含有跨代指針。

Remembered Set 只是一種抽象的數據機構,根據不同的記錄粒度,有不同的具體實現。

  • 字長精度:每個記錄精確到一個機器字長
    (處理器的尋址位數,如32位、64位)

  • 對象精度:每個記錄精確到一個對象

  • 卡精度:每個記錄精確到一塊內存區域 (卡表)

在 HotSpot 中,默認的卡表標記邏輯如下:

CARD_TABLE [this adress >> 9] = 0

這意味着,卡頁的大小是2的9次冪,512字節。

以新生代的 Card Table 爲例,Card Table 的每一個元素用來 標記 老年代的某一塊內存區域(Card Page)的所有對象是否引用了新生代對象。

只要存在一個對象引用了新生代對象,那麼將對應 Card Table 的數組元素的值標記爲 0,說明這個元素變髒(Dirty)。

以圖1爲例,新生代的Card Table 和 Card Page 如下圖所示:

img

那麼,在 新生代GC 的時候,不需要全量掃描老年代的內存空間,只需要篩選出 Card Table 中標記爲 0 的元素,掃描老年代指定範圍的內存塊。

例如,圖2 中的 Page1 和 Page2。

和頁表有點像

寫屏障

我們上面講了利用卡表來記錄對應的老生代內存中的對象引用年輕代對象的情況 , 那麼什麼時候標記呢 ? 怎麼標記呢?

什麼時候標記 ?
肯定是在有其他分代區域中對象引用了本區域對象時,其對應的卡表元素就應該變髒,變髒時間點原則上應該發生在引用類型字段賦值的那一刻.

那麼如何標記呢 ?
jvm 對引用賦值操作的時候 ,進行 AOP (切面編程) , 稱之爲 "寫屏障"

這裏的寫屏障和 volatile 中的不同 ,不是同一個東西, 這裏的寫屏障特指 JVM 爲實現更新卡表而進行的 AOP

併發標記的實現過程

併發標記的內容實際就是 CMS 收集器上運用的技術 .
首先要弄明白 ,併發標記的動機是什麼? 同步標記不行嗎 ? 問題和難點又在哪裏

前面講的gc-root 那些標記 , 以前的垃圾回收器都是會 stop the world , 例如下面的垃圾回收器 :

img

都是把用戶線程按下暫停鍵的, 我們可以看到以前的垃圾回收器都是同步標記 ,這是假如不是同步標記 ,那麼有可能在標記的時候引用關係發生了變化, 那麼就有可能發生錯誤, 就像我們事務一樣, 得保證獨立事務數據纔不受影響, 但是同步標記肯定是沒有併發標記的效率高 (一個不恰當的比喻就是 : 打掃房間時 ,那麼此時的另外一個人就不能再產生垃圾了, 手頭的工作得停下 ; 另外一種情況就是打掃房間時 , 還可以邊產生垃圾 ,邊工作,那麼效率肯定高點)

三色標記法

哪三色

  • 白色:尚未訪問過。
  • 黑色:本對象已訪問過,而且本對象 引用到 的其他對象 也全部訪問過了。
  • 灰色:本對象已訪問過,但是本對象 引用到 的其他對象 尚未全部訪問完。全部訪問後,會轉換爲黑色。
    工作過程 :

img

img

併發標記時,即標記期間應用線程還在繼續跑,對象間的引用可能發生變化,多標漏標的情況就有可能發生。

多標-浮動垃圾

img

假設已經遍歷到E(變爲灰色了),此時應用執行了

D.E = null :

D > E 的引用斷開
此刻之後,對象E/F/G是“應該”被回收的。然而因爲E已經變爲灰色了,其仍會被當作存活對象繼續遍歷下去。最終的結果是:這部分對象仍會被標記爲存活,即本輪GC不會回收這部分內存。
這部分本應該回收 但是 沒有回收到的內存,被稱之爲“浮動垃圾”。浮動垃圾並不會影響應用程序的正確性,只是需要等到下一輪垃圾回收中才被清除。
另外,針對併發標記開始後的新對象,通常的做法是直接全部當成黑色,本輪不會進行清除。這部分對象期間可能會變爲垃圾,這也算是浮動垃圾的一部分。

即是多標導致浮動垃圾 ,雖然會佔用內存, 但也不會出現程序意外.

漏標-程序異常

假設GC線程已經遍歷到E(變爲灰色了),此時應用線程先執行了:

var G = objE.fieldG; 
objE.fieldG = null;  // 灰色E 斷開引用 白色G 
objD.fieldG = G;  // 黑色D 引用 白色G

img

步驟一 : E -> G 斷開
步驟二 : D引用到 G

即是跳過了掃描 ,而顏色沒有變黑

此時切回GC線程繼續跑,因爲E已經沒有對G的引用了,所以不會將G放到灰色集合;儘管因爲D重新引用了G,但因爲D已經是黑色了,不會再重新做遍歷處理。

漏標必須要同時滿足以下兩個條件:

  • 賦值器插入了一條或者多條從黑色對象到白色對象的新引用; (對應步驟二)

  • 賦值器刪除了全部從灰色對象到該白色對象的直接或間接引用。 (對應步驟一)
    這兩個條件必須全部滿足,纔會出現對象消失的問題。那麼我們只需要對上面條件進行破壞,破壞其中的任意一個,都可以防止對象消失問題的產生。這樣就產生了兩種解決方案:

  • 增量更新:Incremental Update。 (記錄下新增的引用關係)

  • 原始快照:Snapshot At The Beginning,SATB。 (記錄下刪除前的引用關係)

增量更新破壞的是第一個條件,當黑色對象插入新的指向白色對象的引用時,就將這個新加入的引用記錄下來,待併發標記完成後,重新對這種新增的引用記錄進行掃描;

原始快照破壞的是第二個條件,當灰色對象要刪除指向白色對象的引用關係時,也是將這個記錄下來,併發標記完成後,對該記錄進行重新掃描。

增量更新與原始快照在 HotSpot 中都有實際應用,其中

  • 增量更新用在 CMS 中
  • 原始快照用在了 G1、Shenandoah 等回收器中。

解決漏標的情況-增量更新

還是上面的那個例子, 漏標的例子
img

解決方案 , 如果新引用的對象 newobj 沒有被標記,那麼就將其標記後堆到標記棧裏。換句話說, 如果 newobj 是白色對象,就把它塗成灰色。這樣操作後的結果如下圖所示:

img

另外一種解決方法 :

img

解決漏標的情況-原始快照

之前灰色引用到到的白色節點 ,記錄下來後變成灰色 , 然灰色節點繼續染色.
img

下面章節來自參考資料 : https://www.cnblogs.com/hongdada/p/14578950.html

三色標記法與現代垃圾回收器

現代追蹤式(可達性分析)的垃圾回收器幾乎都借鑑了三色標記的算法思想,儘管實現的方式不盡相同:比如白色/黑色集合一般都不會出現(但是有其他體現顏色的地方)、灰色集合可以通過棧/隊列/緩存日誌等方式進行實現、遍歷方式可以是廣度/深度遍歷等等。

對於讀寫屏障,以Java HotSpot VM爲例,其併發標記時對漏標的處理方案如下:

  • CMS:寫屏障 + 增量更新
  • G1:寫屏障 + SATB(原始快照)
  • ZGC:讀屏障
    工程實現中,讀寫屏障還有其他功能,比如寫屏障可以用於記錄跨代/區引用的變化,讀屏障可以用於支持移動對象的併發執行等。功能之外,還有性能的考慮,所以對於選擇哪種,每款垃圾回收器都有自己的想法。

值得注意的是,CMS中使用的增量更新,在重新標記階段,除了需要遍歷 寫屏障的記錄,還需要重新掃描遍歷GC Roots(當然標記過的無需再遍歷了),這是由於CMS對於astore_x等指令不添加寫屏障的原因,具體可參考這裏

參考資料

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