在本篇中,我們將繼續探討虛擬機自動內存管理系統的最重要一塊職能:虛擬機如何對死亡的對象進行內存回收。
本篇里面,所有涉及到具體JVM實現的內容,仍然默認為基于HotSpot虛擬機的實現,后文不再單獨說明。
對象存活的判定
當一個對象不會再被使用的時候,我們會說這對象已經死亡。對象何時死亡,寫程序的人應當是最清楚的。如果計算機也要弄清楚這件事情,就需要使用一些方法來進行對象存活判定,常見的方法有引用計數(Reference Counting)有可達性分析(Reachability Analysis)兩種。
引用計數算法的大致思想是給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。它的實現簡單,判定效率也很高,在大部分情況下它都是一個不錯的算法,也有一些比較著名的應用案例,例如微軟COM(Component Object Model)技術、使用ActionScript 3的FlashPlayer、Python語言和在游戲腳本領域得到許多應用的Squirrel中都使用了引用計數算法進行內存管理。但是,至少Java語言里面沒有選用引用計數算法來管理內存,其中最主要原因是它沒有一個優雅的方案去對象之間相互循環引用的問題:當兩個對象互相引用,即使它們都無法被外界使用時,它們的引用計數器也不會為0。
許多主流程序語言中(如Java、C#、Lisp),都是使用可達性分析來判定對象是否存活的。這個算法的基本思路就是通過一系列的稱為GC根節點(GC Roots)的對象作為起始點,從這些節點開始進行向下搜索,搜索所走過的路徑成為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。如圖1所示,對象object 5、object 6、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()方法的編譯后的本地代碼
安全點
在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指令時就會停頓等待,這樣一條指令便完成線程中斷了。
代碼清單2 輪詢指令
安全區域
使用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)收集器為例,介紹內存回收的具體過程。
轉載地址:http://www.infoq.com/cn/articles/jvm-memory-collection
本篇里面,所有涉及到具體JVM實現的內容,仍然默認為基于HotSpot虛擬機的實現,后文不再單獨說明。
對象存活的判定
當一個對象不會再被使用的時候,我們會說這對象已經死亡。對象何時死亡,寫程序的人應當是最清楚的。如果計算機也要弄清楚這件事情,就需要使用一些方法來進行對象存活判定,常見的方法有引用計數(Reference Counting)有可達性分析(Reachability Analysis)兩種。
引用計數算法的大致思想是給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。它的實現簡單,判定效率也很高,在大部分情況下它都是一個不錯的算法,也有一些比較著名的應用案例,例如微軟COM(Component Object Model)技術、使用ActionScript 3的FlashPlayer、Python語言和在游戲腳本領域得到許多應用的Squirrel中都使用了引用計數算法進行內存管理。但是,至少Java語言里面沒有選用引用計數算法來管理內存,其中最主要原因是它沒有一個優雅的方案去對象之間相互循環引用的問題:當兩個對象互相引用,即使它們都無法被外界使用時,它們的引用計數器也不會為0。
許多主流程序語言中(如Java、C#、Lisp),都是使用可達性分析來判定對象是否存活的。這個算法的基本思路就是通過一系列的稱為GC根節點(GC Roots)的對象作為起始點,從這些節點開始進行向下搜索,搜索所走過的路徑成為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。如圖1所示,對象object 5、object 6、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指令時就會停頓等待,這樣一條指令便完成線程中斷了。
代碼清單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
安全區域
使用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)收集器為例,介紹內存回收的具體過程。
轉載地址:http://www.infoq.com/cn/articles/jvm-memory-collection
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
