這里所提到的到都是GC涉及到的一些概念,具體不同的收集器由於內存結構不同,並發串行不同,實現上不止下面這些東西
STW GC中Stop the world
即在執行垃圾收集算法時,Java應用程序的其他所有除了垃圾收集收集器線程之外的線程都被掛起。此時,系統只能允許GC線程進行運行,其他線程則會全部暫停,等待GC線程執行完畢后才能再次運行。jvm虛擬機調優的目的:盡量避免full gc
GC的過程
- 垃圾對象:沒有被引用的對象,沒有GCRoots鏈條上的對象
- 非垃圾對象:被引用的對象
可達性分析算法
難點:
- 查找的過程,光方法區就常有數百上千兆,逐個檢查效率太低
- 查找的過程,是必須要暫停用戶線程。
安全點(能說大概)
- 來由:由於查找的過程需要暫停用戶線程,不可能程序運行每條指令都執行一遍垃圾回收。
- 定義:在特定的位置才會進行GC,這些位置就叫做安全點。
- 選取原則:可以讓程序一直安全的跑下去,不能讓太多的死對象占據內存。
- 遇到長時間執行的指令前就給它 GC 一下,類如方法的調用、循環跳轉、異常跳轉等,遇到類似的執行才會生成 Safepoint
- 實現方式(這里的多線程是指運行同一份代碼無法確定運行順序的解決方案):
- 搶占式中斷:不管你現在在哪里,先給你中斷,然后一看,噢原來你不在安全點,就恢復線程讓你跑一會,然后又中斷直到你到了安全點再 GC。(已經沒有虛擬機會用這個)
- 主動式中斷:需要 GC 的時候,不需要強制中斷線程了,只需要在安全點設置一個輪詢標識,線程只需要去輪詢這個標識即可,線程到安全點了,自己主動中斷,進而 GC。
安全區域(能說大概)
針對主動式中斷,假如我有一個線程正好block住,它沒有在運行,就不能去輪詢 GC 標識了,我們難道要等到它運行再 GC,不可能的對吧!
反過來想一下就是,假如線程本身就不再執行,那何必去管它呢,因為它不可能使引用發生變化啊。故我們又定義了一個安全區域的概念,在這個代碼片段之中發生 GC 都是可以的,因為引用不曾改變。這就是擴大版的安全點啊,也就是它會把block的相關代碼點設為安全區域
在線程執行到安全區域中的代碼時,首先標識自己已經進入了安全區域,那樣,當這段時間內 JVM 要發起 GC 時,就不管沒到安全點但是在安全區域的線程。在線程要離開安全區域時,要檢查系統是否已經完成了 GC,如果完成了,那就繼續執行,否則就要等待 GC 結束的標識之后才可以離開安全區域。
記憶集(能說大概)
學會卡表和寫屏障的概念
如果發現老年代的內存區域存在新生代的引用,那么將會將對象加入記憶集。垃圾回收時,記憶集里面的對象會加入GCroot,就避免了老年代的全遍歷回收。
記憶集的實現方式是通過 卡表的方式
- card Table:值01,若為1說明該卡頁內對象存在指向收集區域的指針
- card Page:存地址值
- 映射關系:card Table[address>>9] 類似hashmap
何時記錄變臟呢?
在引用類型字段賦值的那一刻,通過機器碼層面的手段,把維護卡表的動作放到每一個賦值操作之中。也叫寫屏障
並發可達性分析(必備)
根節點枚舉所帶來的停頓是非常短暫且相對固定的,但是GC Roots往下遍歷對象圖,這一步驟的時間與堆容量成正比,因此采用並發掃描。然而並發掃描存在並發問題如下:
垃圾收集器從 GC Roots 開始標記的過程示意圖如下:
三色標記(也可能會考):
- 白色:對象尚未被垃圾收集器訪問過(若在分析結束后,對象仍為白色,則表示不可達)
- 黑色:對象已被垃圾收集器訪問過,且該對象所有引用都已被掃描(安全存活的)
- 灰色:對象已被垃圾收集器訪問過,但未掃描完所有引用(即該對象正在被掃描,可理解為中間態)
注意引用是有方向的。
但是,如果在標記過程中,用戶線程對引用關系做了修改,如下:
在上圖的(4)中:
- 原先對象 A 未引用 C,對象 B 引用了 C;
- 但標記到 B 時,用戶線程斷開了 B 到 C 的引用,而使 A 引用了 C;
- 則垃圾收集器標記完成后,C 依然是白色(即會被回收掉);
- 對象 DEFG 同理。
這樣導致的后果就是:正在被對象 A 和 D 引用的對象 C 和 G,在垃圾收集器標記的過程中,由於用戶線程的運行,導致本應存活的對象被垃圾收集器標記為消亡、並回收了。程序會因此報錯,這是個嚴重的問題。
如何解決對象消失(主要掌握這個)
如何解決上述“對象消失”的問題呢?理論證明,當且僅當以下兩個條件同時滿足時,才會產生“對象消失”的問題:
- 賦值器插入了一條或多條從黑色對象到白色對象的新引用;
- 賦值器刪除了全部從灰色對象到該白色對象的直接或間接引用。
針對這兩個條件,在上圖中,以對象 A、B、C 為例解釋如下:
若只增加了 A 對 C 的引用,則 C 在垃圾回收后依然是存活的,不會出錯。
若只有 B 斷開了對 A 的引用,則 C 在垃圾回收后是消亡的,但並沒有 A 對 C 的引用,因此也不會出錯。
因此,要解決並發掃描時的對象消失問題,只需破壞其中一個即可。由此產生了兩種解決方案:增量更新(Increment Update)和原始快照(Snapshot At The Begining, SATB)。
增量更新
- 思路:破壞第一個條件。
- 做法:黑色對象(A)插入新的指向白色對象(C)的引用關系(A→C)時,就將這個新插入的引用記錄下來,待並發掃描結束之后,再以這些記錄過的引用關系中的黑色為根,重新掃描一次。
- 簡化理解:黑色對象一旦新插入了指向白色對象的引用,它就變為灰色(需重新掃描)了。
原始快照
- 思路:破壞第二個條件。
- 做法:當灰色對象(B)要刪除指向白色對象(C)的引用關系(B→C)時,就將這個要刪除的引用記錄下來,並發掃描結束后,再以這些記錄過的引用關系中的灰色對象為根,重新掃描一次(可能會有一些垃圾對象但是也沒關系)。
- 簡化理解:無論引用關系刪除與否,都會按照剛開始掃描那一刻的對象圖快照來進行搜索。
這兩種方案都有在用:在 HotSpot 虛擬機中,CMS 是基於增量更新來做並發標記的,G1、Shenandoah 則是用原始快照實現的。
舉例
以上圖為例:在並發掃描時,增加了 A→C 引用,並且刪除了 B→C 引用,若不采取任何措施,則掃描結束后對象 C 會消失。
兩種解決方案的做法分別如下:
- 增量更新:將已標記為黑色的對象 A 置為灰色,待並發掃描結束后,重新掃描對象 A。此時可以掃描到 A→C 引用,對象 C 不會消失。
- 原始快照:若要刪除 B→C 引用,則將原始的 B→C 引用記錄下來(原始的快照),待並發掃描結束后,重新掃描對象 B,由於記錄的是原始信息,其中包含 B→C 引用。這樣,即便未掃描到 A→C 引用,對象 C 也不會消失。
此外,無論引用關系記錄的插入還是刪除,虛擬機都是通過寫屏障實現的
參考來源