java虛擬機(6)垃圾回收算法實現細節 根節點枚舉 安全點 安全區域 記憶集和卡片 寫屏障 並行的可達性分析

根節點枚舉

以可達性分析算法中從GC Roots集合找引用鏈這個操作作爲介紹虛擬機高效實現的第一個例子。

固定可作爲GC Roots的節點主要在全局性的引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中。目標明確,但Java應用越做越龐大,方法區的大小就常有數百上千兆,裏面的類、常量等更是恆河沙數,檢查以這裏爲起源的引用需耗費大量時間。

主流Java虛擬機使用的都是準確式垃圾收集 ,直接得到哪些地方存放着對象引用的。

HotSpot使用一組稱爲OopMap的數據結構來達到這個目的。類加載動作完成的時,會把對象內什麼偏移量上是什麼類型的數據計算出來,在即時編譯過程中,在特定的位置記錄下棧裏和寄存器裏哪些位置是引用。這樣收集器在掃描時就可以直接得知這些信息了,不需要一個不漏地從方法區等GC Roots開始查找。

安全點

HotSpot 沒有爲每條指令都生成OopMap, 只是在“特定的位置”記錄了引用信息,這些位置被稱爲安全點(Safepoint)。

程序執行時並非在代碼指令流的任意位置都能夠停頓下來開始垃圾收集,而是強制要求必須執行到達安全點後才能夠暫停。

安全點的選定

  • 不能太少以至於讓收集器等待時間過長;
  • 不能太過頻繁以至於過分增大運行時的內存負荷。

如何在垃圾收集發生時讓所有線程都跑到最近的安全點?

1、搶先式中斷(Preemptive Suspension)

​ 不需要線程的執行代碼主動去配合,在垃圾收集發生時,系統首先把所有用戶線程全部中斷;發現有用戶線程中斷的地方不在安全點上,就恢復這條線程執行,一會再重新中斷,直到跑到安全點。

2、主動式中斷(Voluntary Suspension)

​ 當垃圾收集需要中斷線程的時候,不直接對線程操作,設置一個標誌位,各個線程執行過程時會不停地主動去輪詢這個標誌,發現中斷標誌爲真時就在最近的安全點上主動中斷掛起。

輪詢標誌的地方和安全點是重合的,另外還要加上所有創建對象和其他需要在Java堆上分配內存的地方。從而檢查是否即將要發生垃圾收集,避免沒有足夠內存分配新對象。

解決問題:安全點機制保證了程序執行時,在不太長的時間內就會遇到可進入垃圾收集過程的安全點。

缺陷:程序“不執行”的時候,即沒有分配處理器時間(如用戶線程處於Sleep狀態或者Blocked狀態),這時線程無法響應虛擬機的中斷請求,不能再走到安全的地方去中斷掛起自己,虛擬機也不可能持續等待線程重新被激活分配處理器時間。對於這種情況,就必須引入安全區域(Safe Region)來解決。

安全區域

安全區域是指能夠確保在某一段代碼片段之中,引用關係不會發生變化。這個區域中任意地方開始垃圾收集都是安全的,可以把安全區域看作被擴展拉伸了的安全點。

當用戶線程執行到安全區域裏面的代碼時,首先會標識自己已經進入了安全區域。這段時間裏虛擬機要發起垃圾收集時就不必去管這些已聲明自己在安全區域內的線程了。當線程要離開安全區域時,它要檢查虛擬機是否已經完成了根節點枚舉(或垃圾收集過程中其他需要暫停用戶線程的階段)。

  • 完成了, 繼續執行;
  • 否則,一直等待至收到可以離開安全區域的信號 。

記憶集和卡片

記憶集是一種用於記錄從非收集區域指向收集區域的指針集合的抽象數據結構。

在垃圾收集的場景中,收集器通過記憶集判斷出某一塊非收集區域,是否存在指向了收集區域的指針,不需要了解跨代指針的全部細節。

記錄精度

  • 字長精度:每個記錄精確到一個機器字長(處理器的尋址位數,如常見的32位或64位,決定了機器訪問物理內存地址的指針長度),該字包含跨代指針。

  • 對象精度:每個記錄精確到一個對象,該對象裏有字段含有跨代指針。

  • 卡精度:每個記錄精確到一塊內存區域,該區域內有對象含有跨代指針。

“卡精度”是用一種稱爲“卡表”(CardTable)的方式去實現記憶集 ,是最常用的一種記憶集實現形式。

卡表最簡單的形式可以只是一個字節數組 , 字節數組每一個元素都對應着其標識的內存區域中一塊特定大小的內存塊,這個內存塊被稱作“卡頁”(CardPage)。

關於卡表與記憶集的關係,可按照Java語言中HashMap與Map的關係來類比理解。

元素變髒(Dirty):一個卡頁的內存中通常包含多個對象,卡頁內有一個(或更多)對象的字段存在跨代指針,就將對應卡表的數組元素的值標識爲 1,若無則標識爲0.

在垃圾收集發生時,只要篩選出卡表中變髒的元素,就能輕易得出哪些卡頁內存塊中包含跨代指針,把它們加入GC Roots中一併掃描。

卡表元素如何維護的問題,例如它們何時變髒、誰來把它們變髒?

有其他分代區域中對象引用了本區域對象時,其對應的卡表元素就應該變髒,變髒時間點原則上應該發生在引用類型字段賦值的一刻。

如何變髒,在對象賦值的一刻去更新維護卡表呢?

解釋執行的字節碼, 虛擬機負責每條字節碼指令的執行,有充分的介入空間;

編譯執行 ,經過即時編譯後的代碼已經是純粹的機器指令流了,這就必須找到一個在機器碼層面的手段,把維護卡表的動作放到每一個賦值操作之中,即寫屏障

寫屏障

寫屏障可以看作在虛擬機層面對“引用類型字段賦值”這個動作的AOP切面 ,在引用對象賦值時會產生一個環形(Around)通知,供程序執行額外的動作,也就是說賦值的前後都在寫屏障的覆蓋範疇內。

在賦值前的部分的寫屏障叫作寫前屏障(Pre-Write Barrier),在賦值後的則叫作寫後屏障(Post-Write Barrier)。

應用寫屏障後,虛擬機會爲所有賦值操作生成相應的指令,一旦收集器在寫屏障中增加了更新卡表操作,無論更新的是不是老年代對新生代的引用,每次只要對引用進行更新,就會產生額外的開銷。

僞共享問題

高併發場景下“僞共享(False Sharing)”問題:多線程修改互相獨立的變量時,如果這些變量恰好共享一個緩存行,會彼此影響而導致性能降低。

“僞共享”問題解決:先檢查卡表標記,僅當該卡表元素未被標記過時纔將其標記爲變髒。

並行的可達性分析

可達性分析算法理論上要求全過程都基於一個能保障一致性的快照中才能進行分析,這意味着必須全程凍結用戶線程(Stop The World)。

爲什麼必須在一個能保證一致性的快照上才能進行對象圖的遍歷呢?

1、用戶線程是凍結的,沒問題;

2、用戶線程沒凍結,也就是用戶線程與收集器併發工作呢?收集器在對象圖標記,同時用戶線程在修改引用關係(修改對象圖的結構),這樣可能出現兩種後果:

  • 原本消亡的對象錯誤標記爲存活,這種情況雖不好(產生了浮動垃圾),但還可以容忍。
  • 原本存活的對象標記爲消亡,這就很嚴重了,程序肯定會因此報錯。

可達性分析的掃描過程如下所示:

上圖三色含義:

  • 白色:對象尚未被垃圾收集器訪問過(若在分析結束後,對象仍爲白色,則表示不可達)
  • 黑色:對象已被垃圾收集器訪問過,且該對象所有引用都已被掃描(安全存活的)
  • 灰色:對象已被垃圾收集器訪問過,但未掃描完所有引用(即該對象正在被掃描,可理解爲中間態)

如何解決上述“對象消失”的問題呢?

“對象消失”的問題產生條件:

1、賦值器插入了一條或多條從黑色對象到白色對象的新引用;

2、賦值器刪除了全部從灰色對象到該白色對象的直接或間接引用。

破壞產生條件之一,便可解決問題,由此產生兩種解決方案:

  • 增量更新(Increment Update)

    破壞的是第一個條件,黑色對象插入新的指向白色對象的引用關係時,將這個新插入的引用記錄,等併發掃描結束之後,將這些記錄過的引用關係中的黑色對象爲根,重新掃描一次。

    簡化理解:黑色對象一旦新插入了指向白色對象的引用之後,就變回灰色對象。

  • 原始快照(Snapshot At The Begining, SATB)。

    破壞的是第二個條件,灰色對象要刪除指向白色對象的引用關係時,將這個要刪除的引用記錄下來。併發掃描結束之後,將這些記錄過的引用關係中的灰色對象爲根,重新掃描一次。

    簡化理解:無論引用關係刪除與否,都按照剛剛開始掃描那一刻的對象圖快照來進行搜索。

以上無論是對引用關係記錄的插入還是刪除,虛擬機的記錄操作都是通過寫屏障實現的。

在HotSpot虛擬機中,增量更新和原始快照這兩種解決方案都有實際應用,譬如,CMS是基於增量更新來做併發標記的,G1、Shenandoah則是用原始快照來實現。

以上主要是爲了稍後介紹各款垃圾收集器時做前置知識鋪墊,如果感到枯燥或者疑惑,可跳過去,等後續遇到要使用它們的實際場景、實際問題時再結合問題,重新翻閱和理解。

歡迎點贊/評論,你們的贊同和鼓勵是我寫作的最大動力!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章