淺談 JVM GC 的安全點與安全區域

小新是也發表於2021-09-12

OopMap

前文我們說到,JVM 採用的可達性分析法有個缺點,就是從 GC Roots 找引用鏈耗時。

都說他耗時,他究竟耗時在哪裡?
GC 進行掃描時,需要檢視每個位置儲存的是不是引用型別,如果是,其所引用的物件就不能被回收;如果不是,那就是基本型別,這些肯定是不會引用物件的;這種對 GC 無用的基本型別的資料非常多,每次 GC 都要去掃描,顯然是非常浪費時間的。
而且迄今為止,所有收集器在 GC Roots 列舉這一步驟都是必須暫停使用者執行緒的。

那有沒有辦法減少耗時呢?
一個很自然的想法,能不能用空間換時間? 把棧上的引用型別的位置全部記錄下來,這樣到 GC 的時候就可以直接讀取,而不用一個個掃描了。Hotspot 就是這麼實現的,這個用於儲存引用型別的資料結構叫 OopMap
OopMap 這個詞可以拆成兩部分:OopMapOop 的全稱是 Ordinary Object Pointer 普通物件指標,Map 大家都知道是對映表,組合起來就是 普通物件指標對映表。

OopMap 的協助下,HotSpot 就能快速準確地完成 GC Roots 列舉啦。

安全點

OopMap 的更新,從直觀上來說,需要在物件引用關係發生變化的時候修改。不過導致引用關係變化的指令非常多,如果對每條指令都記錄 OopMap 的話 ,那將會需要大量的額外儲存空間,空間成本就會變得無法忍受的高昂。選用一些特定的點來記錄就能有效的縮小需要記錄的資料量,這些特定的點就稱為 安全點 (Safepoint)

有了安全點,當 GC 回收需要停止使用者執行緒的時候,將設定某個中斷標誌位,各個執行緒不斷輪詢這個標誌位,發現需要掛起時,自己跑到最近的安全點,更新完 OopMap 才能掛起。這主動式中斷的方式是絕大部分現代虛擬機器選擇的方案,另一種搶佔式就不介紹了。

安全點不是任意的選擇,既不能太少以至於讓收集器等待時間過長,也不能過多以至於過分增大執行時的記憶體負荷。通常選擇一些執行時間較長的指令作為安全點,如方法呼叫迴圈跳轉異常跳轉等。

安全區域

使用安全點的設計似乎已經完美解決如何停頓使用者執行緒,讓虛擬機器進入垃圾回收狀態的問題了。但是,如果此時執行緒正處於 Sleep 或者 Blocked 狀態,該怎麼辦?這些執行緒他不會自己走到安全點,就停不下來了。這個時候,安全點解決不了問題,需要引入 安全區域 (Safe Region)

安全區域指的是,在某段程式碼中,引用關係不會發生變化,執行緒執行到這個區域是可以安全停下進行 GC 的。因此,我們也可以把 安全區域 看做是擴充套件的安全點。

當使用者執行緒執行到安全區域裡面的程式碼時,首先會標識自己已經進入了安全區域。那樣當這段時間裡虛擬機器要發起 GC 時,就不必去管這些在安全區域內的執行緒了。當執行緒要離開安全區域時,它要檢查虛擬機器是否處於 STW 狀態,如果是,則需要等待直到恢復。

總結

HotSpot 使用 OopMap 把引用型別的指標記錄下來,讓 GC Roots 的列舉變得快速準確。
為了減少更新 OopMap 的開銷,引入了 安全點。GC STW 時,執行緒需要跑到距離自己最近的安全點,更新完 OopMap 才能掛起。
處於Sleep 或者 Blocked 狀態的執行緒無法跑到安全點,需要引入安全區域。GC 的時候,不會去管處於安全區域的執行緒,執行緒離開安全區域的時候,如果處於 STW 則需要等待直至恢復。

相關文章