垃圾回收分為兩步:1)判定對象是否存活。2)將“消亡”的對象進行內存回收。
1 判定對象存活
可達性分析算法:通過一系列“GC Roots”對象作為起始節點集,從這些節點開始,根據引用關系向下搜索,搜索過程所走的路徑為“引用鏈”,如果某個對象到“GC Roots”沒有任何引用鏈相連,則判定該對象“消亡”。
強引用 | 正常引用,可達性分析搜索的只有強引用 |
軟引用 | 關聯還有用,但非必須的對象。在系統將要發生內存溢出時,會把這些對象列入回收范圍。 |
弱引用 | 比軟引用更弱,只能生存到下次垃圾收集發生之前。 |
虛引用 | 最弱,無法通過虛引用來取得一個對象實例。唯一作用是在則會個對象被回收時收到一個系統通知。 |
表 Java 的引用類型
1.1 三色標記
圖 可達性分析算法“三色標記”演示過程
1.1.1“對象消失”
垃圾回收器與用戶線程并發執行,若以下兩個條件同時成立,對象消失必然發生。
- 用戶線程將黑色對象指向一個白色對象。
- 用戶刪除所有從灰色對象到該白色對象的引用。
注意:對于在并發期間新建的對象,JVM會把其標記為黑色或灰色。
1.1.2 寫屏障
寫屏障是一段嵌入在對象引用賦值操作中的代碼邏輯(類似AOP)。JVM通過寫屏障實現兩種方式來解決上面“對象消失”的問題。
增量更新 | 破壞第1個條件。當黑色對象插入新指向到白色對象時,寫屏障將新插入引用記錄下來,等并發掃描結束,再將記錄過的引用關系中黑色對象集作為根,重新掃描一次。 效率更低。 |
原始快照 | 破壞第2個條件。當灰色對象要刪除指向白色對象的引用關系時,寫屏障將要刪除的引用關系記錄下來,等并發掃描結束,再將記錄過的引用關系中白色對象集作為根,重新掃描一次。 會產生浮動垃圾。 |
表 “對象消失”的解決方案
1.2 GC Roots 對象
主要有:1) 棧中引用的對象。2)本地方法棧中引用的對象。3)方法區中靜態屬性引用的對象及常量引用的對象。4)JVM 內部的引用。5)被同步鎖持有的對象等。
獲取GC Roots 集必須在一個能保障一致性的快照中才能進行,因此需要STW(Stop The Word)。
1.2.1 棧幀
棧幀是單個線程在方法調用時在棧中分配的內存區域,用于存儲方法的執行狀態。每個方法從調用到執行完成,對應一個棧幀的入棧和出棧。
圖 棧幀內存布局
局部變量表 | 存儲方法的參數、局部變量以及部分中間結果。 以變量槽(Slot)為基本單位,每個Slot占用4個字節。對于8個字節的變量,占用兩個連續的Slot。 索引分配: 非靜態方法第0位Slot存儲this引用。 方法參數從第1位依次存儲。 局部變量按聲明順序分配Slot。 |
操作數棧 | 存儲方法執行過程中的操作數。 |
動態鏈接 | 存儲指向運行時常量池中該方法的符號引用。 |
方法返回地址 | 存儲方法退出后需要返回到的調用者位置。 |
附加信息 | 行號表、局部變量表描述符等。 |
表 棧幀內存組成
1.2.2 OopMap
收集線程需要遍歷方法棧中每一個棧幀,來收集被引用的變量。如果對棧幀的局部變量表進行全表掃描,很耗時。
OopMap(ordinary object pointer Map)普通對象地圖,用于描述棧幀中對象引用的位置。它通常是一個位圖,每個位對應局部變量表中一個槽位。1表示該槽位有對象引用,0表示沒有。例如,假設局部變量表一共有8個槽位,其中只有第1個及第3個槽位有對象引用,則OopMap表示為10100000。
一個棧幀包含多個OopMap。
1.2.3 安全點
引發引用關系變化的指令很多,無法為每一條指令都生成對應的OopMap,只會在某些位置生成,這些位置被稱為安全點。
安全點選擇的原則:平衡線程響應速度和性能開銷。
安全點常用位置:方法調用、循環末尾、異常處理路徑等。
主動式中斷 | 主流方案,當需要GC Roots收集時,JVM在內存中設置一個標識位,用戶線程每次到達安全點都會輪詢標識位,如果需要中斷,則主動掛起。 |
被動式中斷 | 通過操作系統信號強制中斷線程,如果有用戶線程未到達安全點,則恢復該線程,讓其到達安全點后再中斷。 |
表 安全點的實現方案
缺陷:
1)本地方法無法設置安全點。
2)對于未插入安全點但需要長時間執行的指令(如循環),如果在循環提中未插入安全點,則需要等待循環完成才能到達安全點。
3)如果線程在安全點被阻塞或sleep,因為其被喚醒的時間不能確定,JVM無法等到該線程到達安全點。
4)如果為線程阻塞或sleep指定插入安全點,則需要插入安全點的地方會增加,會加重程序的負擔。
1.2.5 安全區域
在某段代碼片段中,引用關系不會發生變化,這個區域任何地方開始收集GC Roots都是安全的,這個區域稱為安全區域。
當用戶線程執行到安全區域時,會標識自己已進入安全區域。這段時間里JVM要進行GC Roots收集就可不必中斷在安全區域內的線程,當線程要離開安全區域時,會先檢測JVM是否完成了GC Roots枚舉,如果完成,則線程繼續執行,否則等待。
安全區域的應用場景:
- 本地代碼的執行,當用戶線程進入本地方法時就標識自己進入了安全區域。
- 統一管理線程多種阻塞狀態,只有線程處于阻塞狀態,即視為進入安全區域。
- 避免“長時間無安全點”的僵局,如在循環體中沒有安全點,則標識為進入了安全區域。
安全區域是對安全點的必要補充。
1.3 對象回收判定
要正式宣告對象死亡,最少要經歷兩次標記過程:
- 可達性分析后進行第1次標記。
- 對上面標記的對象進行篩選,如果這些對象實現了finalize()方法,則會調用這個方法,這是對象唯一次復活的機會,否則宣告對象死亡。