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

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 則需要等待直至恢復。

Tags: