列舉GCRoots的實現
列舉根節點
從可達性分析中從GC Roots節點找引用鏈這個操作為例,可作為GC Roots的節點主要在全域性性的引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變數表)中,現在很多應用僅僅方法區就有數百兆,如果要逐個檢查這裡面的引用,那麼必然會消耗很多時間。另外,可達性分析對執行時間的敏感還體現在GC停頓上,因為這項分析工作必須在一個能確保一致性的快照中進行——這裡“一致性”的意思是指在整個分析期間整個執行系統看起來就像被凍結在某個時間點上,不可以出現分析過程中物件引用關係還在不斷變化的情況,該點不滿足的話分析結果準確性就無法得到保證。這點是導致GC進行時必須停頓所有Java執行執行緒(Sun將這件事情稱為“Stop The World”)的其中一個重要原因,即使是在號稱(幾乎)不會發生停頓的CMS收集器中,列舉根節點時也是必須要停頓的。
由於目前的主流Java虛擬機器使用的都是準確式GC,所以當執行系統停頓下來後,並不需要一個不漏地檢查完所有執行上下文和全域性的引用位置,虛擬機器應當是有辦法直接得知哪些地方存放著物件引用。在HotSpot的實現中,是使用一組稱為OopMap的資料結構來達到這個目的的,在類載入完成的時候,HotSpot就把物件內什麼偏移量上是什麼型別的資料計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和暫存器中哪些位置是引用。這樣,GC在掃描時就可以直接得知這些資訊了。
下面是HotSpot Client VM生成的一段String.hashCode()方法的原生程式碼,可以看到在0x026eb7a9處的call指令有OopMap記錄,它指明瞭EBX暫存器和棧中偏移量為16的記憶體區域中各有一個普通物件指標(Ordinary Object Pointer)的引用,有效範圍為從call指令開始直到0x026eb730(指令流的起始位置)+142(OopMap記錄的偏移量)=0x026eb7be,即hlt指令為止。
[Verified Entry Point] 0x026eb730:mov%eax,-0x8000(%esp) …… ;ImplicitNullCheckStub slow case 0x026eb7a9:call 0x026e83e0 ;OopMap{ebx=Oop[16]=Oop off=142} ;*caload ;-java.lang.String:hashCode@48(line 1489) ;{runtime_call} 0x026eb7ae:push$0x83c5c18 ;{external_word} 0x026eb7b3:call 0x026eb7b8 0x026eb7b8:pusha 0x026eb7b9:call 0x0822bec0;{runtime_call} 0x026eb7be:hlt
安全點
在OopMap的協助下,HotSpot可以快速且準確地完成GC Roots列舉,但一個很現實的問題隨之而來:可能導致引用關係變化,或者說OopMap內容變化的指令非常多,如果為每一條指令都生成對應的OopMap,那將會需要大量的額外空間,這樣GC的空間成本將會變得很高。
實際上,HotSpot也的確沒有為每條指令都生成OopMap,只是在“特定的位置”記錄了這些資訊,這些位置稱為安全點(Safepoint),即程式執行時並非在所有地方都能停頓下來開始GC,只有在到達安全點時才能暫停。Safepoint的選定既不能太少以致於讓GC等待時間太長,也不能過於頻繁以致於過分增大執行時的負荷。所以,安全點的選定基本上是以程式“是否具有讓程式長時間執行的特徵”為標準進行選定的——因為每條指令執行的時間都非常短暫,程式不太可能因為指令流長度太長這個原因而過長時間執行,“長時間執行”的最明顯特徵就是指令序列複用,例如方法呼叫、迴圈跳轉、異常跳轉等,所以具有這些功能的指令才會產生Safepoint。
對於Sefepoint,另一個需要考慮的問題是如何在GC發生時讓所有執行緒(這裡不包括執行JNI呼叫的執行緒)都“跑”到最近的安全點上再停頓下來。
這裡有兩種方案可供選擇:搶先式中斷(Preemptive Suspension)和主動式中斷(Voluntary Suspension)。
搶先式中斷不需要執行緒的執行程式碼主動去配合,在GC發生時,首先把所有執行緒全部中斷,如果發現有執行緒中斷的地方不在安全點上,就恢復執行緒,讓它“跑”到安全點上。現在幾乎沒有虛擬機器實現採用搶先式中斷來暫停執行緒從而響應GC事件。
而主動式中斷的思想是當GC需要中斷執行緒的時候,不直接對執行緒操作,僅僅簡單地設定一個標誌,各個執行緒執行時主動去輪詢這個標誌,發現中斷標誌為真時就自己中斷掛起。輪詢標誌的地方和安全點是重合的,另外再加上建立物件需要分配記憶體的地方。
下面的test指令是HotSpot生成的輪詢指令,當需要暫停執行緒時,虛擬機器把0x160100的記憶體頁設定為不可讀,執行緒執行到test指令時就會產生一個自陷異常訊號,在預先註冊的異常處理器中暫停執行緒實現等待,這樣一條彙編指令便完成安全點輪詢和觸發執行緒中斷。
0x01b6d627:call 0x01b2b210;OopMap{[60]=Oop off=460} ;*invokeinterface size ;-Client1:main@113(line 23) ;{virtual_call} 0x01b6d62c:nop ;OopMap{[60]=Oop off=461} ;*if_icmplt ;-Client1:main@118(line 23) 0x01b6d62d:test%eax,0x160100;{poll} 0x01b6d633:mov 0x50(%esp),%esi 0x01b6d637:cmp%eax,%esi
安全區域
使用Safepoint似乎已經完美地解決了如何進入GC的問題,但實際情況卻並不一定。Safepoint機制保證了程式執行時,在不太長的時間內就會遇到可進入GC的Safepoint。但是,程式“不執行”的時候呢?所謂的程式不執行就是沒有分配CPU時間,典型的例子就是執行緒處於Sleep狀態或者Blocked狀態,這時候執行緒無法響應JVM的中斷請求,“走”到安全的地方去中斷掛起,JVM也顯然不太可能等待執行緒重新被分配CPU時間。對於這種情況,就需要安全區域(Safe Region)來解決。
安全區域是指在一段程式碼片段之中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的。我們也可以把Safe Region看做是被擴充套件了的Safepoint。
線上程執行到Safe Region中的程式碼時,首先標識自己已經進入了Safe Region,那樣,當在這段時間裡JVM要發起GC時,就不用管標識自己為Safe Region狀態的執行緒了。線上程要離開Safe Region時,它要檢查系統是否已經完成了根節點列舉(或者是整個GC過程),如果完成了,那執行緒就繼續執行,否則它就必須等待直到收到可以安全離開Safe Region的訊號為止。
相關文章
- c++11 實現列舉值到列舉名的轉換C++
- OC中列舉寫法 以及 字串型別列舉實現探索字串型別
- 遞迴實現指數型列舉遞迴
- js模擬實現列舉效果JS
- C#中實現列舉數C#
- 深入淺出 Java 中列舉的實現原理Java
- 基於註解的 PHP 列舉類實現PHP
- Java一個列舉類的2種實現。Java
- Java 利用列舉實現單例模式Java單例模式
- 透過列舉enum實現單例單例
- 7.1 實現程式記憶體塊列舉記憶體
- 小技巧分享:在 Go 如何實現列舉?Go
- 125 列舉實現PHP擷取中文不亂碼的實現方法PHP
- Java 列舉查詢並不拋異常的實現Java
- 列舉和列舉的取值範圍
- 用Swift列舉完美實現3Dtouch快捷操作Swift3D
- Java 列舉、JPA 和 PostgreSQL 列舉JavaSQL
- TypeScript魔法堂:列舉的超實用手冊TypeScript
- Java列舉-通過值查詢對應的列舉Java
- Java enum列舉類詳解 列舉的常見用法Java
- 為什麼java中用列舉實現單例模式會更好Java單例模式
- iOS 列舉的巧用iOS
- delphi 裡的 列舉
- java中的列舉Java
- Java列舉Java
- Swift,列舉Swift
- 語法糖甜不甜?巧用列舉實現“狀態”轉換限制
- C/C++列舉enum分別列印輸出列舉子和列舉值的方法C++
- Swift列舉的全用法Swift
- Java 列舉 switch的用法Java
- C# 中的“智慧列舉”:如何在列舉中增加行為C#
- C# 列舉與位列舉概述C#
- 列舉工具類
- TypeScript 列舉enumTypeScript
- Java 列舉(enum)Java
- Swift-列舉Swift
- 自定義列舉
- java列舉類Java