從可達性分析中從GC Roots節點找引用鏈這個操做爲例,可做爲GC Roots的節點主要在全局性的引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中,如今不少應用僅僅方法區就有數百兆,若是要逐個檢查這裏面的引用,那麼必然會消耗不少時間。另外,可達性分析對執行時間的敏感還體如今GC停頓上,由於這項分析工做必須在一個能確保一致性的快照中進行——這裏「一致性」的意思是指在整個分析期間整個執行系統看起來就像被凍結在某個時間點上,不能夠出現分析過程當中對象引用關係還在不斷變化的狀況,該點不知足的話分析結果準確性就沒法獲得保證。這點是致使GC進行時必須停頓全部Java執行線程(Sun將這件事情稱爲「Stop The World」)的其中一個重要緣由,即便是在號稱(幾乎)不會發生停頓的CMS收集器中,枚舉根節點時也是必需要停頓的。java
因爲目前的主流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的空間成本將會變得很高。spa
實際上,HotSpot也的確沒有爲每條指令都生成OopMap,只是在「特定的位置」記錄了這些信息,這些位置稱爲安全點(Safepoint),即程序執行時並不是在全部地方都能停頓下來開始GC,只有在到達安全點時才能暫停。Safepoint的選定既不能太少以至於讓GC等待時間太長,也不能過於頻繁以至於過度增大運行時的負荷。因此,安全點的選定基本上是以程序「是否具備讓程序長時間執行的特徵」爲標準進行選定的——由於每條指令執行的時間都很是短暫,程序不太可能由於指令流長度太長這個緣由而過長時間運行,「長時間執行」的最明顯特徵就是指令序列複用,例如方法調用、循環跳轉、異常跳轉等,因此具備這些功能的指令纔會產生Safepoint。線程
對於Sefepoint,另外一個須要考慮的問題是如何在GC發生時讓全部線程(這裏不包括執行JNI調用的線程)都「跑」到最近的安全點上再停頓下來。指針
這裏有兩種方案可供選擇:搶先式中斷(Preemptive Suspension)和主動式中斷(Voluntary Suspension)。code
搶先式中斷不須要線程的執行代碼主動去配合,在GC發生時,首先把全部線程所有中斷,若是發現有線程中斷的地方不在安全點上,就恢復線程,讓它「跑」到安全點上。如今幾乎沒有虛擬機實現採用搶先式中斷來暫停線程從而響應GC事件。對象
而主動式中斷的思想是當GC須要中斷線程的時候,不直接對線程操做,僅僅簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起。輪詢標誌的地方和安全點是重合的,另外再加上建立對象須要分配內存的地方。blog
下面的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的信號爲止。