(轉)Java系列:JVM中的OopMap(zz) - 拿走不謝 - 博客園

轉載自 : https://www.cnblogs.com/strinkbug/p/6376525.html

調用棧裏的引用類型數據是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,運行時需要在對象上帶有足夠的元數據。如果是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、從外部記錄下類型信息,存成映射表。現在三種主流的高性能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就會包含多條記錄。

每個被JIT編譯過後的方法也會在一些特定的位置記錄下OopMap,記錄了執行到該方法的某條指令的時候,棧上和寄存器裏哪些位置是引用。這樣GC在掃描棧的時候就會查詢這些OopMap就知道哪裏是引用了。這些特定的位置主要在: 
1、循環的末尾 
2、方法臨返回前 / 調用方法的call指令後 
3、可能拋異常的位置

這種位置被稱爲“安全點”(safepoint)。之所以要選擇一些特定的位置來記錄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方法的調用比較慢的原因之一。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章