目錄

•寫在前面

•保守式GC

•準確式GC

•補充

•半保守式GC

•JNI方法


•寫在前面

JVM在進行正式GC之前總是需要進行可達性分析來查找內存中所有存活對象,以便能夠正確的回收已經死亡的對象,如果有了解JVM的GC機制的話(不瞭解可以看一下我的另一篇文章,JVM如何判斷對象能否回收),我們就會知道,調用棧裏的引用類型數據時GC的根集合的重要組成部分,找出棧上的引用是GC的根枚舉中不可或缺的一環。而對於一個十分複雜的程序系統,每次進行GC的時候,都要遍歷所有的引用肯定是不現實的,因為在可達性分析中,需要進行Stop The World,程序中的線程需要停止來配合可達性分析。所以,每次直接遍歷整個引用鏈肯定是不現實的。 為了應對這種尷尬的問題,最早有保守式GC和後來的準確式GC。這裏準確式GC就會提到一個OopMap,用來保存類型的映射表。

•保守式GC

在進行GC的時候,會從一些已知的位置(也就是GC Roots)開始掃描內存,掃描到一個數字就判斷他是不是可能是指向GC堆中的一個指針(這裏會涉及上下邊界檢查,GC堆的上下界是已知的、對齊檢查,通常分配空間的時候會有對齊要求,假如説是4字節對齊,那麼不能被4整除的數字就肯定不是指針)。然後一直遞歸的掃描下去,最後完成可達性分析。這種模糊的判斷方法因為無法準確判斷一個位置上是否是真的指向GC堆中的指針,所以被命名為保守式GC。這種可達性分析的方式因為不需要準確的判斷出一個指針,所以效率快,但是也正因為這種特點,它存在下面兩個明顯的缺點:

  • 因為是模糊的檢查,所以對於一些已經死掉的對象,很可能會被誤認為仍有地方引用他們,GC也就自然不會回收他們,從而引起了無用的內存佔用,造成資源浪費。
  • 由於不知道疑似指針是否真的是指針,所以它們的值都不能改寫;移動對象就意味着要修正指針。換言之,對象就不可移動了。有一種辦法可以在使用保守式GC的同時支持對象的移動,那就是增加一個間接層,不直接通過指針來實現引用,而是添加一層“句柄”(handle)在中間,所有引用先指到一個句柄表裏,再從句柄表找到實際對象。這樣,要移動對象的話,只要修改句柄表裏的內容即可。但是這樣的話引用的訪問速度就降低了。

•準確式GC

與保守式GC相對的就是準確式GC,何為準確式GC?就是我們準確的知道,某個位置上面是否是指針,對於java來説,就是知道對於某個位置上的數據是什麼類型的,這樣就可以判斷出所有的位置上的數據是不是指向GC堆的引用,包括棧和寄存器裏的數據。而實現這種要求的方法有很多種,不過,在java中實現的方式是:從外部記錄下類型信息,存成映射表,在HotSpot中把這種映射表稱之為OopMap,不同的虛擬機名稱可能不一樣,簡而言之,OopMap就是存着一系列信息的數據結構。實現這種功能,需要虛擬機的解釋器和JIT編譯器支持,由他們來生成OopMap。生成這樣的映射表一般有兩種方式:

  • 每次都遍歷原始的映射表,循環的一個個偏移量掃描過去;這種用法也叫“解釋式”; 
  • 為每個映射表生成一塊定製的掃描代碼(想像掃描映射表的循環被展開的樣子),以後每次要用映射表就直接執行生成的掃描代碼;這種用法也叫“編譯式”。

總而言之,GC開始的時候,就通過OopMap這樣的一個映射表知道,在對象內的什麼偏移量上是什麼類型的數據,而且特定的位置記錄下棧和寄存器中哪些位置是引用。

•補充

準確式GC中,記錄並判斷指針是否指向堆另外的幾種實現方式

  • 讓數據自身帶上標記(tag)。這種做法在JVM裏不常見,但在別的一些語言實現裏有體現。打標記的方式在半保守式GC中倒是更常見一些,例如CRuby就是用打標記的半保守式GC。
  • 讓編譯器為每個方法生成特別的掃描代碼。

•半保守式GC

JVM可以選擇在棧上不記錄類型信息,而在對象上記錄類型信息。這樣的話,掃描棧的時候仍然會跟上面説的過程一樣,但掃描到GC堆內的對象時因為對象帶有足夠類型信息了,JVM就能夠判斷出在該對象內什麼位置的數據是引用類型了。這種是“半保守式GC”,也稱為“根上保守。

要説明一下的是,每個方法可能會有好幾個oopMap,就是根據safepoint(安全點,見我另一篇文章)把一個方法的代碼分成幾段,每一段代碼一個oopMap,作用域自然也僅限於這一段代碼。 循環中引用多個對象,肯定會有多個變量,編譯後佔據棧上的多個位置,那這段代碼的oopMap就會包含多條記錄。

•JNI方法

對Java線程中的JNI方法,它們既不是由JVM裏的解釋器執行的,也不是由JVM的JIT編譯器生成的,所以會缺少OopMap信息。那麼GC碰到這樣的棧幀該如何維持準確性呢? HotSpot的解決方法是:所有經過JNI調用邊界(調用JNI方法傳入的參數、從JNI方法傳回的返回值)的引用都必須用“句柄”(handle)包裝起來。JNI需要調用Java API的時候也必須自己用句柄包裝指針。在這種實現中,JNI方法裏寫的“jobject”實際上不是直接指向對象的指針,而是先指向一個句柄,通過句柄才能間接訪問到對象。這樣在掃描到JNI方法的時候就不需要掃描它的棧幀了——只要掃描句柄表就可以得到所有從JNI方法能訪問到的GC堆裏的對象。但這也就意味着調用JNI方法會有句柄的包裝/拆包裝的開銷,是導致JNI方法的調用比較慢的原因之一。