1. 程式人生 > 其它 >深入理解JVM - HotSpot 實現細節

深入理解JVM - HotSpot 實現細節

1. 根節點列舉

迄今為止,所有收集器在根節點列舉這一步驟時都是必須暫停使用者執行緒的。
現在可達性分析演算法耗時最長的查詢引用鏈的過程已經可以做到與使用者執行緒一起併發。
但根節點列舉始終還是必須在一個能保障一致性的快照中才得以進行。

目前主流Java虛擬機器使用的都是準確式垃圾收集,虛擬機器應當是有辦法直接得到哪些地方存放著物件引用。
在 HotSpot 的解決方案裡,是使用一組稱為 OopMap 的資料結構來達到這個目的。一旦類載入動作完成的時候,HotSpot就會把物件內什麼偏移量上是什麼型別的資料計算出來,在即時編譯過程中,也會在特定的位置記錄下棧裡和暫存器裡哪些位置是引用。這樣收集器在掃描時就可以直接得知這些資訊了,並不需要真正一個不漏地從方法區等GC Roots開始查詢。

2. 安全點

HotSpot 只在特定位置生成了 OopMap,這些位置即為安全點 Safepoint
那麼程式執行時就需要使用者程式執行到安全點才能暫停使用者程式。
因此安全點的選定既不能太少以至於讓收集器等待時間過長,也不能太過頻繁以至於過分增大執行時的記憶體負荷。
安全點位置的選取基本上是以“是否具有讓程式長時間執行的特徵”為標準 進行選定的,因為每條指令執行的時間都非常短暫,程式不太可能因為指令流長度太長這樣的原因而長時間執行,“長時間執行”的最明顯特徵就是指令序列的複用,例如方法呼叫、迴圈跳轉、異常跳轉等都屬於指令序列複用,所以只有具有這些功能的指令才會產生安全點。

如何在垃圾收集發生時讓所有執行緒(這裡其實不包括執行JNI呼叫的執行緒)都跑到最近的安全點,然後停頓下來

  1. 搶先式中斷 Preemptive Suspension: 發生GC時,系統中斷所有使用者執行緒,若有執行緒中斷點不在安全點,恢復此執行緒的執行,直到再安全點中斷。
  2. 主動式中斷 Voluntary Suspension: GC要發生是置位標誌,各使用者執行緒執行時主動輪詢標誌位,然後到安全點主動掛起。輪詢標誌的地方和安全點是重合的,另外還要加上所有建立物件和其他需要在Java堆上分配記憶體的地方,這是為了檢查是否即將要發生垃圾收集,避免沒有足夠記憶體分配新物件。

由於輪詢操作在程式碼中會頻繁出現,這要求它必須足夠高效。HotSpot使用記憶體保護陷阱的方式,把輪詢操作精簡至只有一條彙編指令的程度。下面程式碼中的test指令就是HotSpot生成的輪詢指令,當需要暫停使用者執行緒時,虛擬機器把0x160100的記憶體頁設定為不可讀,那執行緒執行到test指令時就會產生一個自陷異常訊號,然後在預先註冊的異常處理器中掛起執行緒實現等待,這樣僅通過一條彙編指令便完成安全點輪詢和觸發執行緒中斷了.

3. 安全區域

安全點保證了程式執行時的GC,但是程式不執行時呢?
安全區域是指能夠確保在某一段程式碼片段之中,引用關係不會發生變化,因此,在這個區域中任意地方開始垃圾收集都是安全的。我們也可以把安全區域看作被擴充套件拉伸了的安全點。
當用戶執行緒執行到安全區域裡面的程式碼時,首先會標識自己已經進入了安全區域,那樣當這段時間裡虛擬機器要發起垃圾收集時就不必去管這些已宣告自己在安全區域內的執行緒了。當執行緒要離開安全 區域時,它要檢查虛擬機器是否已經完成了根節點列舉(或者垃圾收集過程中其他需要暫停使用者執行緒的 階段),如果完成了,那執行緒就當作沒事發生過,繼續執行;否則它就必須一直等待,直到收到可以 離開安全區域的訊號為止。

4. 記憶集與卡表

為解決物件跨代引用所帶來的問題,垃圾收集器在新生代中建立了名為記憶集(Remembered Set)的資料結構,用以避免把整個老年代加進GC Roots掃描範圍。
事實上並不只是新生代、老年代之間才有跨代引用的問題,所有涉及部分割槽域收集(Partial GC)行為的 垃圾收集器,典型的如G1、ZGC和Shenandoah收集器,都會面臨相同的問題。
記憶集是一種用於記錄從非收集區域指向收集區域的指標集合的抽象資料結構。
在垃圾收集的場景中,收集器只需要通過記憶集判斷出某一塊非收集區域是否存在有指向了收集區域的指標就可以了,並不需要了解這些跨代指標的全部細節.
可供選擇的精度

  • 每個記錄精確到一個機器字長(就是處理器的定址位數,如常見的32位或64位,這個精度決定了機器訪問實體記憶體地址的指標長度),該字包含跨代指標
  • 物件精度:每個記錄精確到一個物件,該物件裡有欄位含有跨代指標
  • 卡精度:每個記錄精確到一塊記憶體區域,該區域內有物件含有跨代指標
    卡精度”所指的是用一種稱為“卡表”(Card Table)的方式去實現記憶集,目前最常用的一種記憶集實現形式。

卡表最簡單的形式可以只是一個位元組陣列,而HotSpot虛擬機器確實也是這樣做的。以下這行程式碼是HotSpot預設的卡表標記邏輯

CARD_TABLE [this address >> 9] = 0;

位元組陣列CARD_TABLE的每一個元素都對應著其標識的記憶體區域中一塊特定大小的記憶體塊,這個記憶體塊被稱作“卡頁”(Card Page)。一般來說,卡頁大小都是以2的N次冪的位元組數,通過上面程式碼可以看出HotSpot中使用的卡頁是2的9次冪,即512位元組(地址右移9位,相當於用地址除以512)。那如果卡表標識記憶體區域的起始地址是0x0000的話,陣列CARD_TABLE的第0、1、2號元素,分別對應了地址範圍為0x0000~0x01FF、0x0200~0x03FF、0x0400~0x05FF的卡頁記憶體塊。
一個卡頁的記憶體中通常包含不止一個物件,只要卡頁內有一個(或更多)物件的欄位存在著跨代指標,那就將對應卡表的陣列元素的值標識為1,稱為這個元素變髒(Dirty),沒有則標識為0。在垃圾收集發生時,只要篩選出卡表中變髒的元素,就能輕易得出哪些卡頁記憶體塊中包含跨代指標,把它們加入GC Roots中一併掃描。

5. 寫屏障

使用記憶集縮減了GC Roots掃描範圍,那麼卡表如何維護?
何時變髒:有其他分代區域中物件引用了本區域物件時,其對應的卡表元素就應該變髒,變髒時間點原則上應該發生在引用型別欄位賦值的那一刻。
如何變髒:通過寫屏障(Write Barrier)技術維護卡表狀態的,寫屏障即對“引用型別欄位賦值”這個動作的AOP切面。在賦值前的部分的寫屏障叫作寫前屏障(Pre-Write Barrier),在賦值 後的則叫作寫後屏障(Post-Write Barrier)。

void oop_field_store(oop* field, oop new_value) {
    // 引用欄位賦值操作 
    *field = new_value; 
    // 寫後屏障,在這裡完成卡表狀態更新
    post_write_barrier(field, new_value);
}

卡表在高併發場景下還面臨著偽共享(False Sharing)問題,一種簡單的解決方案是不採用無條件的寫屏障,而是先檢查卡表標記,只有當該卡表元素未被標記過時才將其標記為變髒。
在JDK 7之後,HotSpot虛擬機器增加了一個新的引數-XX:+UseCondCardMark,用來決定是否開啟卡表更新的條件判斷。開啟會增加一次額外判斷的開銷,但能夠避免偽共享問題,兩者各有效能損 耗,是否開啟要根據應用實際執行情況來進行測試權衡。

6. 併發可達性分析

“標記”階段是所有追蹤式垃圾收集演算法的共同特徵。
為什麼必須在一個能保障一致性的快照上才能進行物件圖的遍歷?三色標記法

  • 白色:表示物件尚未被垃圾收集器訪問過。顯然在可達性分析剛剛開始的階段,所有的物件都是白色的,若在分析結束的階段,仍然是白色的物件,即代表不可達
  • 黑色: 表示物件已經被垃圾收集器訪問過,且這個物件的所有引用都已經掃描過。黑色的物件代表已經掃描過,它是安全存活的,如果有其他物件引用指向了黑色物件,無須重新掃描一遍。黑色物件不可能直接(不經過灰色物件)指向某個白色物件。
  • 灰色:表示物件已經被垃圾收集器訪問過,但這個物件上至少存在一個引用還沒有被掃描過

若標記過程與使用者執行緒併發,可能出現的問題:一種是把原本消亡的物件錯誤標記為存活, 這不是好事,但其實是可以容忍的,只不過產生了一點逃過本次收集的浮動垃圾而已,下次收集清理 掉就好。另一種是把原本存活的物件錯誤標記為已消亡,這就是非常致命的後果了,程式肯定會因此發生錯誤。

Wilson於1994年在理論上證明了,當且僅當以下兩個條件同時滿足時,會產生“物件消失”的問題,即原本應該是黑色的物件被誤標為白色

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

因此,我們要解決併發掃描時的物件消失問題,只需破壞這兩個條件的任意一個即可。由此分別產生了兩種解決方案:增量更新(Incremental Update)原始快照(Snapshot At The Beginning, SATB)

增量更新
破壞第一個條件,當黑色物件插入新的指向白色物件的引用關係時,就將這個新插入的引用記錄下來,等併發掃描結束之後,再將這些記錄過的引用關係中的黑色物件為根,重新掃描一次。這可以簡化理解為,黑色物件一旦新插入了指向白色物件的引用之後,它就變回灰色物件 了。

原始快照
破壞第二個條件,當灰色物件要刪除指向白色物件的引用關係時,就將這個要刪除的引用記錄下來,在併發掃描結束之後,再將這些記錄過的引用關係中的灰色物件為根,重新掃描一次。這也可以簡化理解為,無論引用關係刪除與否,都會按照剛剛開始掃描那一刻的物件圖快照來進行搜尋。

以上無論是對引用關係記錄的插入還是刪除,虛擬機器的記錄操作都是通過寫屏障實現的。在HotSpot虛擬機器中,增量更新和原始快照這兩種解決方案都有實際應用,譬如,CMS是基於增量更新 來做併發標記的,G1、Shenandoah則是用原始快照來實現。