調用棧裏的引用類型數據是GC的根集合(root set)的重要組成部分;找出棧上的引用是GC的根枚舉(root enumeration)中不可或缺的一環。
java
JVM選擇用什麼方式會影響到GC的實現:android
若是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的缺點有:
一、會有部分對象原本應該已經死了,但有疑似指針指向它們,使它們逃過GC的收集。這對程序語義來講是安全的,由於全部應該活着的對象都會是活的;但對內存佔用量來講就不是件好事,總會有一些已經不須要的數據還佔用着GC堆空間。具體實現能夠經過一些調節來讓這種無用對象的比例少一些,能夠緩解(但不能根治)內存佔用量大的問題。數據結構
二、因爲不知道疑似指針是否真的是指針,因此它們的值都不能改寫;移動對象就意味着要修正指針。換言之,對象就不可移動了。有一種辦法能夠在使用保守式GC的同時支持對象的移動,那就是增長一個間接層,不直接經過指針來實現引用,而是添加一層「句柄」(handle)在中間,全部引用先指到一個句柄表裏,再從句柄表找到實際對象。這樣,要移動對象的話,只要修改句柄表裏的內容便可。可是這樣的話引用的訪問速度就下降了。Sun JDK的Classic VM用過這種全handle的設計,但效果實在算不上好。編程語言
因爲JVM要支持豐富的反射功能,原本就須要讓對象能瞭解自身的結構,而這種信息GC也能夠利用上,因此不多有JVM會用徹底保守式的GC。除非真的是特別懶…oop
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堆裏的引用,包括活動記錄(棧+寄存器)裏的數據。
有幾種辦法:
一、讓數據自身帶上標記(tag)。這種作法在JVM裏不常見,但在別的一些語言實現裏有體現。就不詳細介紹了。打標記的方式在半保守式GC中卻是更常見一些,例如CRuby就是用打標記的半保守式GC。CLDC-HI比較有趣,棧上對每一個slot都配對一個字長的tag來講明它的類型,經過這種方式來減小stack map的開銷;相似的實如今別的地方沒怎麼見過,你們通常都不這麼取捨。
二、讓編譯器爲每一個方法生成特別的掃描代碼。我還沒見過JVM實現裏這麼作的,雖然說在別的語言實現裏有見過。
三、從外部記錄下類型信息,存成映射表。如今三種主流的高性能JVM實現,HotSpot、JRockit和J9都是這樣作的。其中,HotSpot把這樣的數據結構叫作OopMap,JRockit裏叫作livemap,J9裏叫作GC map。Apache Harmony的DRLVM也把它叫GCMap。
要實現這種功能,須要虛擬機裏的解釋器和JIT編譯器都有相應的支持,由它們來生成足夠的元數據提供給GC。
使用這樣的映射表通常有兩種方式:
一、每次都遍歷原始的映射表,循環的一個個偏移量掃描過去;這種用法也叫「解釋式」;
二、爲每一個映射表生成一塊定製的掃描代碼(想像掃描映射表的循環被展開的樣子),之後每次要用映射表就直接執行生成的掃描代碼;這種用法也叫「編譯式」。
在HotSpot中,對象的類型信息裏有記錄本身的OopMap,記錄了在該類型的對象內什麼偏移量上是什麼類型的數據。因此從對象開始向外的掃描能夠是準確的;這些數據是在類加載過程當中計算獲得的。
能夠把oopMap簡單理解成是調試信息。 在源代碼裏面每一個變量都是有類型的,可是編譯以後的代碼就只有變量在棧上的位置了。oopMap就是一個附加的信息,告訴你棧上哪一個位置原本是個什麼東西。 這個信息是在JIT編譯時跟機器碼一塊兒產生的。由於只有編譯器知道源代碼跟產生的代碼的對應關係。 每一個方法可能會有好幾個oopMap,就是根據safepoint把一個方法的代碼分紅幾段,每一段代碼一個oopMap,做用域天然也僅限於這一段代碼。 循環中引用多個對象,確定會有多個變量,編譯後佔據棧上的多個位置。那這段代碼的oopMap就會包含多條記錄。
每一個被JIT編譯事後的方法也會在一些特定的位置記錄下OopMap,記錄了執行到該方法的某條指令的時候,棧上和寄存器裏哪些位置是引用。這樣GC在掃描棧的時候就會查詢這些OopMap就知道哪裏是引用了。這些特定的位置主要在:
一、循環的末尾
二、方法臨返回前 / 調用方法的call指令後
三、可能拋異常的位置
這種位置被稱爲「安全點」(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方法的調用比較慢的緣由之一。