【轉】JVM內存回收理論與實現

     本篇裏面,全部涉及到具體JVM實現的內容,仍然默認爲基於HotSpot虛擬機的實現,後文再也不單獨說明。 

對象存活的斷定  
當一個對象不會再被使用的時候,咱們會說這對象已經死亡。對象什麼時候死亡,寫程序的人應當是最清楚的。若是計算機也要弄清楚這件事情,就須要使用一些方法來進行對象存活斷定,常見的方法有引用計數(Reference Counting)和可達性分析(Reachability Analysis)兩種。 
引用計數算法的大體思想是給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的。它的實現簡單,斷定效率也很高,在大部分狀況下它都是一個不錯的算法,也有一些比較著名的應用案例,例如微軟COM(Component Object Model)技術、使用ActionScript 3的FlashPlayer、Python語言和在遊戲腳本領域獲得許多應用的Squirrel中都使用了引用計數算法進行內存管理。可是,至少Java語言(這裏指HotSpot等主流的JVM)裏面沒有選用引用計數算法來管理內存,其中最主要緣由是它沒有一個優雅的方案去對象之間相互循環引用的問題:當兩個對象互相引用,即便它們都沒法被外界使用時,它們的引用計數器也不會爲0。 
許多主流程序語言中(如Java、C#、Lisp),都是使用可達性分析來斷定對象是否存活的。這個算法的基本思路就是:經過一系列的稱爲GC根節點(GC Roots)的對象做爲起始點,從這些節點開始進行向下搜索,搜索時所走過的路徑成爲引用鏈(Reference Chain)。當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來講就是從GC Roots到這個對象不可達)時,則證實此對象是不可用的。如圖1所示,對象object 五、object 六、object 7雖然互相有關聯,它們的引用並不爲0,可是它們到GC Roots是不可達的,所以它們將會被斷定爲是可回收的對象。

 
圖1 可達性分析算法斷定對象是否可回收

枚舉根節點  
在Java語言裏面,可做爲GC Roots的節點主要在全局性的引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中。若是要使用可達性分析來判斷內存是否可回收的,那分析工做必須在一個能保障一致性的快照中進行——這裏「一致性」的意思是整個分析期間整個執行系統看起來就像被凍結在某個時間點上,不能夠出現分析過程當中,對象引用關係還在不斷變化的狀況,這點不知足的話分析結果準確性就沒法保證。這點也是致使GC進行時必須「Stop The World」的其中一個重要緣由,即便是號稱(幾乎)不會發生停頓的CMS收集器中,枚舉根節點時也是必需要停頓的。 
因爲目前的主流JVM使用的都是準確式GC,因此當執行系統停頓下來以後,並不須要一個不漏地檢查完全部執行上下文和全局的引用位置,虛擬機應當是有辦法直接獲得哪些地方存放着對象引用。在HotSpot的實現中,是使用一組稱爲OopMap的數據結構來達到這個目的,在類加載完成的時候,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程當中,也會在特定的位置記錄下棧裏和寄存器裏哪些位置是引用。這樣GC在掃描時就就能夠直接得知這些信息了。下面的代碼清單1是HotSpot Client VM生成的一段String.hashCode()方法的本地代碼,能夠看到在0x026eb7a9處的call指令有OopMap記錄,它指明瞭EBX寄存器和棧中偏移量爲16的內存區域中各有一個普通對象指針(Ordinary Object Pointer)的引用,有效範圍爲從call指令開始直到0x026eb730(指令流的起始位置)+142(OopMap記錄的偏移量)=0x026eb7be,即hlt指令爲止。 

代碼清單1 String.hashCode()方法的編譯後的本地代碼    
[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須要中斷線程的時候,不直接對線程操做,僅僅簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起。輪詢標誌的地方和安全點是重合的,另外再加上建立對象須要分配內存的地方。下面的代碼清單2中的test指令是HotSpot生成的輪詢指令,當須要暫停線程時,虛擬機把0x160100的內存頁設置爲不可讀,那線程執行到test指令時就會停頓等待,這樣一條指令便完成線程中斷了。 java

代碼清單2 輪詢指令   算法

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

安全區域 shell

使用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的信號爲止。 
到這裏,咱們簡單介紹了虛擬機如何去發起內存回收的問題,可是虛擬機如何具體地進行內存回收動做仍然未涉及到。由於內存回收如何進行是由虛擬機所採用的GC收集器所決定的,而一般虛擬機中每每不止有一種GC收集器,像目前(JDK 7時代)的HotSpot裏面就包含有Serial、Serial Old、ParNew、Parallel Scavenge、Parallel Old、Concurrent Mark Sweep和Garbage First七種收集器,在下一篇中,咱們將以最新最早進的Garbage First(G1)收集器爲例,介紹內存回收的具體過程。 
相關文章
相關標籤/搜索