新聞中心
- 根節(jié)點枚舉
- 從根節(jié)點開始遍歷對象圖
前文我們在介紹垃圾收集算法的時候,簡單提到過:標(biāo)記-整理算法(Mark-Compact)中的移動存活對象操作是一種極為負(fù)重的操作,必須全程暫停用戶應(yīng)用程序才能進行,像這樣的停頓被最初的虛擬機設(shè)計者形象地描述為 “Stop The World (STW)”。

成都創(chuàng)新互聯(lián)公司-專業(yè)網(wǎng)站定制、快速模板網(wǎng)站建設(shè)、高性價比旌陽網(wǎng)站開發(fā)、企業(yè)建站全套包干低至880元,成熟完善的模板庫,直接使用。一站式旌陽網(wǎng)站制作公司更省心,省錢,快速模板網(wǎng)站建設(shè)找我們,業(yè)務(wù)覆蓋旌陽地區(qū)。費用合理售后完善,十多年實體公司更值得信賴。
顯然 STW 并不是一件好事,能夠避免那就需要盡可能避免。
在可達(dá)性分析中,第一階段 ”可達(dá)性分析“ 是必須 STW 的,而第二階段 ”從根節(jié)點開始遍歷對象圖“,如果不進行 STW 的話,會導(dǎo)致一些問題,由于第二階段時間比較長,長時間的 STW 很影響性能,所以大佬們設(shè)計了一些解決方案,從而使得這個第二階段可以不用 STW,大幅減少時間。
先這樣籠統(tǒng)的介紹下,大伙兒對可達(dá)性分析的整體脈絡(luò)有個認(rèn)識就行,下面會詳細(xì)解釋,我會分兩篇文章來寫,本篇就先來分析第一階段 ”可達(dá)性分析“!
根節(jié)點枚舉
迄今為止,所有收集器在根節(jié)點枚舉這一步驟時都是必須暫停用戶線程的,枚舉過程必須在一個能保障 ”一致性“ 的快照中才得以進行。
通俗來說,整個枚舉期間整個系統(tǒng)看起來就像被凍結(jié)在某個時間點上,不會出現(xiàn)在分析過程中,用戶進程還在運行,導(dǎo)致根節(jié)點集合的對象引用關(guān)系還在不斷變化的情況,若這點都不能滿足的話,可達(dá)性分析結(jié)果的準(zhǔn)確性顯然也就無法保證。
也就是說,根節(jié)點枚舉與我們之前提到的標(biāo)記-整理算法(Mark-Compact)中的移動存活對象操作一樣會面臨相似的 “Stop The World” 的困擾。
另外,眾所周知,可作為 GC Roots 的對象引用就那么幾個,主要在全局性的引用(例如常量或類靜態(tài)屬性)與執(zhí)行上下文(例如虛擬機棧中引用的對象)中,盡管目標(biāo)很明確,但查找過程要做到快速高效其實并不是一件容易的事情。
現(xiàn)在 Java 應(yīng)用越做越龐大,光是方法區(qū)的大小就常有數(shù)百上千兆,里面的類、常量等更是一大堆,要是把這些區(qū)域全都掃描檢查一遍顯然太過于費事。
那有沒有辦法減少耗時呢?
一個很自然的想法,空間換時間!
把引用類型和它對應(yīng)的位置信息用哈希表記錄下來,這樣到 GC 的時候就可以直接讀取這個哈希表,而不用一個區(qū)域一個區(qū)域地進行掃描了。Hotspot 就是這么實現(xiàn)的,這個用于存儲引用類型的數(shù)據(jù)結(jié)構(gòu)叫 OopMap。
下圖是 HotSpot 虛擬機客戶端模式下生成的一段 String::hashCode() 方法的本地代碼,可以看到在 0x026eb7a9 處的 call 指令有 OopMap 記錄,它指明了 EBX 寄存器和棧中偏移量為 16 的內(nèi)存區(qū)域中各有一個 OopMap 的引用,有效范圍為從 call 指令開始直到0x026eb730(指令流的起始位置)+ 142(OopMap 記錄的偏移量)= 0x026eb7be,即 hlt 指令為止。
實話實說,這段不理解也就算了,知道 OopMap 是這么一個東西就行了。
安全點 Safe Point
在 OopMap 的協(xié)助下,HotSpot 可以快速完成根節(jié)點枚舉了,但一個很現(xiàn)實的問題隨之而來:由于引用關(guān)系可能會發(fā)生變化,這就會導(dǎo)致 OopMap 內(nèi)容變化的指令非常多,如果為每一條指令都生成對應(yīng)的 OopMap,那將會需要大量的額外存儲空間,這樣垃圾收集伴隨而來的空間成本就會變得無法忍受的高昂。
所以實際上 HotSpot 也確實沒有為每條指令都生成 OopMap,只是在 “特定的位置” 生成 OopMap,換句話說,只有在某些 ”特定的位置“ 上才會把對象引用的相關(guān)信息給記錄下來,這些位置也被稱為安全點(Safepoint)。
有了安全點的設(shè)定,也就決定了用戶程序執(zhí)行時并不是隨便哪個時候都能夠停頓下來開始 GC 的,而是強制要求程序必須執(zhí)行到達(dá)安全點后才能夠進行 GC(因為不到達(dá)安全點話,沒有 OopMap,虛擬機就沒法快速知道對象引用的位置呀,沒法進行根節(jié)點枚舉)。
如下圖所示:
因此,安全點的設(shè)定既不能太少以至于讓垃圾收集器等待時間過長,也不能太多以至于頻繁進行垃圾收集從而導(dǎo)致運行時的內(nèi)存負(fù)荷大幅增大。所以,安全點的選定基本上是以 “是否具有讓程序長時間執(zhí)行的特征” 為標(biāo)準(zhǔn)進行選定的,最典型的就是指令序列的復(fù)用:例如方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等,所以只有具有這些功能的指令才會產(chǎn)生安全點。
對于安全點,另外一個需要考慮的問題是,如何在 GC 發(fā)生時讓所有用戶線程都執(zhí)行到最近的安全點,然后停頓下來呢?。這里有兩種方案可供選擇:
- 搶先式中斷(Preemptive Suspension):這種思路很簡單,就是在 GC 發(fā)生時,系統(tǒng)先把所有用戶線程全部中斷掉。然后如果發(fā)現(xiàn)有用戶線程中斷的位置不在安全點上,就恢復(fù)這條線程執(zhí)行,直到跑到安全點上再重新中斷。
搶先式中斷的最大問題是時間成本的不可控,進而導(dǎo)致性能不穩(wěn)定和吞吐量的波動,特別是在高并發(fā)場景下這是非常致命的,所以現(xiàn)在幾乎沒有虛擬機實現(xiàn)采用搶先式中斷來暫停線程響應(yīng) GC 事件。
- 主動式中斷(Voluntary Suspension):主動式中斷不會直接中斷線程,而是全局設(shè)置一個標(biāo)志位,用戶線程會不斷的輪詢這個標(biāo)志位,當(dāng)發(fā)現(xiàn)標(biāo)志位為真時,線程會在最近的一個安全點主動中斷掛起?,F(xiàn)在的虛擬機基本都是用這種方式。
安全區(qū)域 Safe Region
安全點機制保證了程序執(zhí)行時,在不太長的時間內(nèi)就會遇到可進入垃圾收集過程的安全點。
對于主動式中斷來說,用戶線程需要不斷地去輪詢標(biāo)志位,那對于那些處于 sleep 或者 blocked 狀態(tài)的線程(不在活躍狀態(tài)的線程)來說怎么辦?
這些不在活躍狀態(tài)的線程沒有獲得 CPU 時間,沒法去輪詢標(biāo)志位,自然也就沒法找到最近的安全點主動中斷掛起了。
換句話說,對于這些不活躍的線程,我們沒法掌控它們醒過來的時間。很可能其他線程都已經(jīng)通過輪詢標(biāo)志位到達(dá)安全點被中斷了,然后虛擬機開始根節(jié)點枚舉了(根節(jié)點枚舉需要暫停所有用戶線程),但是這時候那些本不活躍的用戶線程又醒過來了開始執(zhí)行,破壞了對象之間的引用關(guān)系,那顯然是不行的。
對于這種情況,就必須引入安全區(qū)域(Safe Region)來解決。
安全區(qū)域的定義是這樣的:確保在某一段代碼片段之中,引用關(guān)系不會發(fā)生變化,因此,在這個區(qū)域中的任意地方開始 GC 都是安全的。
可以簡單地把安全區(qū)域看作被拉長了的安全點。
當(dāng)用戶線程執(zhí)行到安全區(qū)域里面的代碼時,首先會標(biāo)識自己已經(jīng)進入了安全區(qū)域。那樣當(dāng)這段時間里虛擬機要發(fā)起 GC 時,就不必去管這些在安全區(qū)域內(nèi)的線程了。當(dāng)安全區(qū)域中的線程被喚醒并離開安全區(qū)域時,它需要檢查下主動式中斷策略的標(biāo)志位是否為真(虛擬機是否處于 STW 狀態(tài)),如果為真則繼續(xù)掛起等待(防止根節(jié)點枚舉過程中這些被喚醒線程的執(zhí)行破壞了對象之間的引用關(guān)系),如果為假則標(biāo)識還沒開始 STW 或者 STW 剛剛結(jié)束,那么線程就可以被喚醒然后繼續(xù)執(zhí)行。
網(wǎng)站名稱:可達(dá)性分析深度剖析:安全點和安全區(qū)域
文章轉(zhuǎn)載:http://m.fisionsoft.com.cn/article/cdgsohe.html


咨詢
建站咨詢
