Jvm中的OopMap以及可達性分析


  最近開始回顧整理一些Jvm的知識點,記錄一下,如有描述不准確的地方還望大家評論指出,共同進步。

一、可達性分析算法

  在Jvm的HotSpot虛擬機中使用的是可達性分析算法來確定內存中的對象是否要被回收,那么首先來說一下可達性分析算法是怎么玩的呢?他的基本思路就是通過一系列成為GC Roots 的根對象作為起始節點集,從這些節點開始,根據引用關系向下搜索,搜索過程所走過的路被稱為引用鏈。

如果某個對象到GC Roots間沒有任何引用鏈相連,那么就證明這個對象是不可能再被使用的,就可以判定這樣的對象是可以被回收的。

  在Java技術體系中,固定可以被作為GC Roots的對象包括以下幾種:

  • 在虛擬機棧(棧的本地變量表)中引用的對象
  • 在方法區中靜態屬性引用的對象
  • 在方法區中常量引用的對象
  • 在本地方法棧中引用的對象
  • Java虛擬機內部的引用,如基本數據類型對應的Class對象,一些常駐的異常對象(NPE),類加載器等等
  • 所有被同步鎖持有的對象
  • 反應Java虛擬機內部情況的JMXBean,JVMTI中注冊的回調,本地代碼緩存等

上面列出的這些都是固定可以作為GC Roots的集合,除此之外根據用戶選取的垃圾收集器以及當前內存回收的區域不同,還可以有其他對象被添加到 GC Roots的集合中,共同作為完成的GC Roots來進行可達性分析的起始點。

 

在可達性分析階段為了方便理解,會將堆中的對象用三種顏色進行標注:

  • 白色:標記的初始階段,所有都想都沒有被掃描
  • 黑色:表示對象已經被掃描過,且所有引用都已經被掃描過
  • 灰色:對象中的一部分引用已經被掃描過

所以我們可以這么去理解:灰色階段是一個中間態的階段,最終所有的對象只能是黑色或者是白色,黑色的對象就是存活的對象,白色的就是需要被清除的。

但是我們知道在進行GC的過程中,除非是STW否則我們無法保證用戶的線程不去改變已經被掃描和確認過的對象,因為GC的進程是與用戶進程並發進行的。

那么這里就有可能出現兩種情況:

  • 原本是白色的對象,被當作了黑色的對象
    • 這樣的結果就是本來應該被清除的對象卻被保留了下來,但是這個結果其實我們是可以接受的,這次沒有被清除的,下次再被清除就可以了,這些對象就是我們經常說的浮動垃圾
  • 原本是黑色的對象,被當作了白色的對象
    • 這個就比較麻煩了,因為一旦被當作成白色的對象,那就是需要被清除的,這樣就會導致程序出現錯誤。

 

Wilson在1994年通過理論證明了,當且僅當同時滿足以下兩個條件的時候,會將原本是黑色的對象誤標記為白色的對象:

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

這里應該這么理解:首先是需要同時滿足這兩個條件,其實這兩個條件說的是一個過程,就是如果一個對象首先斷了與灰色對象全部的直接和間接的聯系,那么這個時候這個對象就肯定是白色的了,然后這個對象再與被一個黑色對象所引用。而我們知道如果在GC標記過程中,已經被標記為黑色的對象是不會被再次掃描的,那么這個對象即使已經被黑色對象所引用,但是因為這個黑色對象不會再次被掃描,所以這個對象仍然是白色的,所以在下次GC執行的時候就會被清除掉,所以就會產生程序級的錯誤了。

 

既然可能出現的問題我們理解清除了,就要知道怎么來處理這種問題。在Hotspot中提供了兩種方式來解決這個問題:

  • 增量更新:當黑色對象添加了指向白色對象的引用關系時,會將這個引用記錄下來,等並發掃描之后,再將這些黑色結果作為根節點重新掃描
  • 原始快照:將從灰色階段刪除的引用記錄下來,掃並發掃描之后再將這些灰色節點作為根節點再掃描一次

 

二、哪些對象會被臨時性的添加到GC Roots中?

  上文提到了,除了固定GC Roots的節點外 ,還會有一些其他節點會被臨時的添加到GC Roots中,那么到底有哪些節點會被臨時的添加進去呢?

這里首先要提到一個分代收集的理論,比如我們這次要回收新生代,或者是G1中我們要回收一個指定的Region的時候,我們肯定是不希望把所有的GC Roots都進行可達性分析的,因為這樣比較浪費資源。

比如我們這里想要回收新生代,那么我們會首先找到存在於新生代中的GC Root節點,通過引用鏈的方式去尋找可達對象,這樣分析完之后我們可以確定哪些對象是可達的,哪些是不可達的。但是這里存在一個問題,

就是這里我們只是選取了存在於新生代中的GC Root節點作為根節點開始的分析,那么剩下的沒有被可達的對象,是有可能存在從老年代中的引用的,所以如果這個時候我們直接把這些沒有能夠從新生代GC Root節點可達的對象

標記為可刪除,那么就有可能誤殺一些對象。

  所以基於上面的分析,在進行新生代回收的時候,我們首先選擇存在於新生代中的GC Root添加到集合中,然后再看有沒有存在於老年代的跨代引用,如果存在那么就需要把老年代中的部分對象作為GC Root添加到集合中,用這些節點一起共同進行可達性分析

通過這種方式進行篩選進行可達性分析之后依然不可達的對象,我們就可以放心的標記為不可達,在后面的回收過程中會把這樣的對象回收掉。

三、怎么知道新生代中的對象是否被老年代引用了呢?

   在Jvm中是通過一種叫做Remembered Set的數據結構來記錄跨代引用的,用以避免在進行新生代GC的時候把整個老年代都添加GCRoots的掃描中。當然這里不僅僅指新生代與老年代中的引用,在后面出現的一些不分代的垃圾收集器中也用到這個數據結構,比如G1中的Region概念等等。

Remembered Set是一種用於記錄從非收集區指向收集區與中指針的集合的一種抽象數據結構,關於怎么記錄這些指向關系,在Jvm中定義了幾種不同的精度:

  • 字長精度:每個記錄精確到一個機器字長(就是處理器的尋址位數),該字包含跨代指針
  • 對象精度:每個記錄精確到一個對象,該對象里有字段漢有跨代指針
  • 卡精度:每個記錄精確到一塊內存區域,該區域有對象含有跨代指針

其中的第三種精度是使用一種叫做卡表的方式實現的Rembered Set,卡表的底層實現就是一個數字,每個數組元素對應的是內存中一塊特定大小的內存塊,這個內存塊被稱為卡頁,,一般來說卡頁的大小都是2的N次冪的字節數,在HotSpot中一個卡頁的大小是2^9,所以對應的在數組中就會把內存區域按照2^9進行划分成多個內存塊。

這樣我們就可以知道在一個卡頁中會存在很多個對象,只要卡頁中有一個對象的字段存在着跨代引用,那么就想卡表對應的數組元素設置為1,這個頁就叫做臟頁。然后在接下來進行垃圾回收的時候,只要首先找到臟頁對應的卡頁,然后再找到里面存在跨代引用的對象,把他們添加到GC Roots中一起進行可達性分析,這樣就可以保證待分析的區域不會出現對象誤殺的情況了。

四、怎么快速找到固定的GC Root呢?

  上面聊了一些關系對象可達性的內容,但是我們知道在有與用戶交互的場景中我們更多的是關心STW的時間,但是現在的Jvm的內存是越來越大了,那么怎么快速的找到GC Root對象呢?很顯然掃描整個方法區或者是虛擬機棧是不現實的想法。

在選取GC Roots的時候最不好確定的就是在棧中的對象,快速的判斷和找出在棧中指向堆中的對象,是影響GC STW的關鍵,就是在這個環節Jvm必須要暫停所有的用戶線程,在一個相對靜止的快照進行分析,所以這個環節在尋找GC Root的用時多少就決定了STW的時間。

這里HotSpot采用了一種OopMap的數據結構來記錄哪個地方存的是引用,這樣在進行GC Root標記的時候,直接掃描OopMap,就可以快速的確定GC Root,從而減少STW的時間了。

  首先我們要知道在OopMap中要記錄兩種情況,一種是對象,一種是方法。如果是對象,那么在這個類被加載完成之后,那么類對象內在什么位置保存的是引用的對象就可以提前知道了,這樣在線程棧中就可以通過OopMap來記錄對應的引用對象的地址信息。第二種就是方法,我們知道在棧中

每個方法對應一個棧幀,在方法的內部會存在引用關系的變化,這時會根據安全點的原則進行OopMap的記錄,這樣在每個線程棧上都有一個對應的OopMap , 通過掃描這些OopMap 其實我們就可以快速的定位到在堆中的GCRoot對象,為后面進行可達性標記分析提供支持。 

五、寫屏障

  前文我們知道通過Remembered Set可以解決跨代引用的問題,但是怎么實現跨代引用出現的時候同時更新Remembered Set的呢?這里HotSopt中采用了寫屏障的技術,首先我們思考一下應該在什么時候更新Remembered Set呢?應該是在對象賦值的那一刻去更新才對,

所以寫屏障就是在引用類型賦值的時候,添加一個AOP切面,這樣在賦值動作發生的時候會產生一個Around通知,而更新Remembered Set的工作就是在這個里面來實現的,所以如果開始了寫屏障是會增加一些時間消耗的。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM