原文地址: http://www.cnblogs.com/strinkbug/p/6376525.html
在讀周智明的深入理解JVM虛擬機時,關於枚舉根節點/安全點這部分感覺書上寫的不是太明白,找了半天感覺這篇文章寫的比書里更好理解,雖然我看的還是很吃力,從一個小小的oopMap數據結構可以窺見hotspot虛擬機垃圾回收的精髓
調用棧里的引用類型數據是GC的根集合(root set)的重要組成部分;找出棧上的引用是GC的根枚舉(root enumeration)中不可或缺的一環。
JVM選擇用什么方式會影響到GC的實現:
如果JVM選擇不記錄任何這種類型的數據,那么它就無法區分內存里某個位置上的數據到底應該解讀為引用類型還是整型還是別的什么。這種條件下,實現出來的GC就會是“保守式GC(conservative GC)”。在進行GC的時候,JVM開始從一些已知位置(例如說JVM棧)開始掃描內存,掃描的時候每看到一個數字就看看它“像不像是一個指向GC堆中的指針”。這里會涉及上下邊界檢查(GC堆的上下界是已知的)、對齊檢查(通常分配空間的時候會有對齊要求,假如說是4字節對齊,那么不能被4整除的數字就肯定不是指針),之類的。然后遞歸的這么掃描出去。(有可能存在數字的值和一個地址的值相同,這樣如果這個地址上的對象本來應該被回收,但是由於這個棧的數字巧合的和它的地址值相同,導致這個對象不會被回收)
保守式GC的好處是相對來說實現簡單些,而且可以方便的用在對GC沒有特別支持的編程語言里提供自動內存管理功能。Boehm-Demers-Weiser GC是保守式GC中的典型代表,可以嵌入到C或C++等語言寫的程序中。
小歷史故事:
微軟的JScript和早期版VBScript也是用保守式GC的;微軟的JVM也是。VBScript后來改回用引用計數了。而微軟JVM的后代,也就是.NET里的CLR,則改用了完全准確式GC。
為了趕上在一個會議上發布消息,微軟最初的JVM原型只有一個月左右的時間從開工到達到符合Java標准。所以只好先用簡單的辦法來實現,也就自然選用了保守式GC。
信息來源:Patrick Dussud在Channel 9的訪談,23分鍾左右
保守式GC的缺點有:
1、會有部分對象本來應該已經死了,但有疑似指針指向它們,使它們逃過GC的收集。這對程序語義來說是安全的,因為所有應該活着的對象都會是活的;但對內存占用量來說就不是件好事,總會有一些已經不需要的數據還占用着GC堆空間。具體實現可以通過一些調節來讓這種無用對象的比例少一些,可以緩解(但不能根治)內存占用量大的問題。
2、由於不知道疑似指針是否真的是指針,所以它們的值都不能改寫;移動對象就意味着要修正指針。換言之,對象就不可移動了。有一種辦法可以在使用保守式GC的同時支持對象的移動,那就是增加一個間接層,不直接通過指針來實現引用,而是添加一層“句柄”(handle)在中間,所有引用先指到一個句柄表里,再從句柄表找到實際對象。這樣,要移動對象的話,只要修改句柄表里的內容即可。但是這樣的話引用的訪問速度就降低了。Sun JDK的Classic VM用過這種全handle的設計,但效果實在算不上好。
由於JVM要支持豐富的反射功能,本來就需要讓對象能了解自身的結構,而這種信息GC也可以利用上,所以很少有JVM會用完全保守式的GC。除非真的是特別懶…
JVM可以選擇在棧上不記錄類型信息,而在對象上記錄類型信息。這樣的話,掃描棧的時候仍然會跟上面說的過程一樣,但掃描到GC堆內的對象時因為對象帶有足夠類型信息了,JVM就能夠判斷出在該對象內什么位置的數據是引用類型了。這種是“半保守式GC”,也稱為“根上保守(conservative with respect to the roots)”。 (我感覺半保守GC並沒有提高枚舉根節點的速度,只是讓對象移動成為可能)
為了支持半保守式GC,運行時需要在對象上帶有足夠的元數據。如果是JVM的話,這些數據可能在類加載器或者對象模型的模塊里計算得到,但不需要JIT編譯器的特別支持。
前面提到了Boehm GC,實際上它不但支持完全保守的方式,也可以支持半保守的方式。GCJ和Mono都是以半保守方式使用Boehm GC的例子。
Google Android的Dalvik VM的早期版本也是使用半保守式GC的一個例子。不過到2009年中的時候Dalvik VM的內部版本就已經開始支持准確式GC了——代價是優化過的DEX文件的體積膨脹了約9%。
其實許多較老的JVM都選擇這種實現方式。
由於半保守式GC在堆內部的數據是准確的,所以它可以在 直接使用指針來實現引用的條件下 支持部分對象的移動(這句有點繞,我中間加了空格斷句),方法是只將保守掃描能直接掃到的對象設置為不可移動(pinned)(可以理解為一級對象,從棧開始第一層掃描到的對象),而從它們出發再掃描到的對象就可以移動了。
完全保守的GC通常使用不移動對象的算法,例如mark-sweep。半保守方式的GC既可以使用mark-sweep,也可以使用移動部分對象的算法,例如Bartlett風格的mostly-copying GC。
半保守式GC對JNI方法調用的支持會比較容易:管它是不是JNI方法調用,是棧都掃過去…完事了。不需要對引用做任何額外的處理。當然代價跟完全保守式一樣,會有“疑似指針”的問題。
與保守式GC相對的是“准確式GC”,原文可以是precise GC、exact GC、accurate GC或者type accurate GC。外國人也挺麻煩的,“准確”都統一不到一個詞上⋯
是什么東西“准確”呢?關鍵就是“類型”,也就是說給定某個位置上的某塊數據,要能知道它的准確類型是什么,這樣才可以合理地解讀數據的含義;GC所關心的含義就是“這塊數據是不是指針”。
要實現這樣的GC,JVM就要能夠判斷出所有位置上的數據是不是指向GC堆里的引用,包括活動記錄(棧+寄存器)里的數據。
有幾種辦法:
1、讓數據自身帶上標記(tag)。這種做法在JVM里不常見,但在別的一些語言實現里有體現。就不詳細介紹了。打標記的方式在半保守式GC中倒是更常見一些,例如CRuby就是用打標記的半保守式GC。CLDC-HI比較有趣,棧上對每個slot都配對一個字長的tag來說明它的類型,通過這種方式來減少stack map的開銷;類似的實現在別的地方沒怎么見過,大家一般都不這么取舍。
2、讓編譯器為每個方法生成特別的掃描代碼。我還沒見過JVM實現里這么做的,雖說在別的語言實現里有見過。
3、從外部記錄下類型信息,存成映射表。(在解釋執行時/JIT時,記錄下棧上某個數據對應的數據類型,比如地址1上的”12344“值是一個堆上地址引用,數據類型為com.aaaa.aaa.AAA)現在三種主流的高性能JVM實現,HotSpot、JRockit和J9都是這樣做的。其中,HotSpot把這樣的數據結構叫做OopMap,JRockit里叫做livemap,J9里叫做GC map。Apache Harmony的DRLVM也把它叫GCMap。
要實現這種功能,需要虛擬機里的解釋器和JIT編譯器都有相應的支持,由它們來生成足夠的元數據提供給GC。
使用這樣的映射表一般有兩種方式:
1、每次都遍歷原始的映射表,循環的一個個偏移量掃描過去;這種用法也叫“解釋式”;
2、為每個映射表生成一塊定制的掃描代碼(想像掃描映射表的循環被展開的樣子),以后每次要用映射表就直接執行生成的掃描代碼;這種用法也叫“編譯式”。 不明白這是什么意思?
在HotSpot中,對象的類型信息里有記錄自己的OopMap,記錄了在該類型的對象內什么偏移量上是什么類型的數據。所以從對象開始向外的掃描可以是准確的;這些數據是在類加載過程中計算得到的。
可以把oopMap簡單理解成是調試信息。 在源代碼里面每個變量都是有類型的,但是編譯之后的代碼就只有變量在棧上的位置了。oopMap就是一個附加的信息,告訴你棧上哪個位置本來是個什么東西。 這個信息是在JIT編譯時跟機器碼一起產生的。因為只有編譯器知道源代碼跟產生的代碼的對應關系。 每個方法可能會有好幾個oopMap,就是根據safepoint把一個方法的代碼分成幾段,每一段代碼一個oopMap,作用域自然也僅限於這一段代碼。(過了作用域oopMap會清除么?) 循環中引用多個對象,肯定會有多個變量,編譯后占據棧上的多個位置。那這段代碼的oopMap就會包含多條記錄。(我理解GC在枚舉根節點的時候就是遍歷這些oopMap數據結構,應該會有多個,因為每個方法是跑在一個線程上的,jvm內部會有多個線程)
每個被JIT編譯過后的方法也會在一些特定的位置記錄下OopMap,記錄了執行到該方法的某條指令的時候,棧上和寄存器里哪些位置是引用。這樣GC在掃描棧的時候就會查詢這些OopMap就知道哪里是引用了。這些特定的位置主要在:
1、循環的末尾
2、方法臨返回前 / 調用方法的call指令后
3、可能拋異常的位置
這種位置被稱為“安全點”(safepoint)。之所以要選擇一些特定的位置來記錄OopMap,是因為如果對每條指令(的位置)都記錄OopMap的話,這些記錄就會比較大,(安全點是個靜態的概念,和事務類似,不管GC執行不執行,到了該增加/更新oopMap的時候就會自動執行,可能oopMap會緩存一段時間才會清除,所以這里說會有空間開銷問題,不知道我理解的對不對)那么空間開銷會顯得不值得。選用一些比較關鍵的點來記錄就能有效的縮小需要記錄的數據量,但仍然能達到區分引用的目的。因為這樣,HotSpot中GC不是在任意位置都可以進入,而只能在safepoint處進入。
而仍然在解釋器中執行的方法則可以通過解釋器里的功能自動生成出OopMap出來給GC用。
平時這些OopMap都是壓縮了存在內存里的;在GC的時候才按需解壓出來使用。
HotSpot是用“解釋式”的方式來使用OopMap的,每次都循環變量里面的項來掃描對應的偏移量。
對Java線程中的JNI方法,它們既不是由JVM里的解釋器執行的,也不是由JVM的JIT編譯器生成的,所以會缺少OopMap信息。那么GC碰到這樣的棧幀該如何維持准確性呢?
HotSpot的解決方法是:所有經過JNI調用邊界(調用JNI方法傳入的參數、從JNI方法傳回的返回值)的引用都必須用“句柄”(handle)包裝起來。JNI需要調用Java API的時候也必須自己用句柄包裝指針。在這種實現中,JNI方法里寫的“jobject”實際上不是直接指向對象的指針,而是先指向一個句柄,通過句柄才能間接訪問到對象。這樣在掃描到JNI方法的時候就不需要掃描它的棧幀了——只要掃描句柄表就可以得到所有從JNI方法能訪問到的GC堆里的對象。
但這也就意味着調用JNI方法會有句柄的包裝/拆包裝的開銷,是導致JNI方法的調用比較慢的原因之一。