淺談 JVM GC 的安全點與安全區域
OopMap
前文我們說到,JVM 採用的可達性分析法有個缺點,就是從 GC Roots
找引用鏈耗時。
都說他耗時,他究竟耗時在哪裡?
GC 進行掃描時,需要查看每個位置存儲的是不是引用類型,如果是,其所引用的對象就不能被回收;如果不是,那就是基本類型,這些肯定是不會引用對象的;這種對 GC 無用的基本類型的數據非常多,每次 GC 都要去掃描,顯然是非常浪費時間的。
而且迄今為止,所有收集器在 GC Roots
枚舉這一步驟都是必須暫停用戶執行緒的。
那有沒有辦法減少耗時呢?
一個很自然的想法,能不能用空間換時間? 把棧上的引用類型的位置全部記錄下來,這樣到 GC 的時候就可以直接讀取,而不用一個個掃描了。Hotspot 就是這麼實現的,這個用於存儲引用類型的數據結構叫 OopMap
。
OopMap
這個詞可以拆成兩部分:Oop
和 Map
,Oop
的全稱是 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 則需要等待直至恢復。