Java instanceof 關鍵字是如何實現的?

yangxi_001發表於2013-12-20
在進入正題前先得提一句:既然樓主提問的背景是“被面試問到”,那很重要的一點是揣摩面試官提問的意圖。按照樓主的簡單描述,面試官問的是“Java的instanceof關鍵字是如何實現的”,那要選擇怎樣的答案就得看面試官有怎樣的背景。比較安全的應對方法是請求面試官澄清/細化他的問題——他想要的“底層”底到什麼程度。

---------------------------------------------------------------------

情形1:
你在面月薪3000以下的Java碼農職位。如果面試官也只是做做Java層開發的,他可能只是想讓你回答Java語言層面的 instanceof 運算子的語義。Java語言的“意思”就已經是“底層”。這樣的話只要參考Java語言規範對 instanceof 運算子的定義就好:
15.20.2 Type Comparison Operator instanceof, Java語言規範Java SE 7版
當然這實際上回答的不是“如何實現的”,而是“如何設計的”。但面試嘛⋯

如果用Java的虛擬碼來表現Java語言規範所描述的執行時語義,會是這樣:
// obj instanceof T
boolean result;
try {
    T temp = (T) obj; // checkcast
    result = true;
} catch (ClassCastException e) {
    result = false;
}

用中文說就是:如果有表示式 obj instanceof T ,那麼如果 (T) obj 不拋 ClassCastException 異常則該表示式值為 true ,否則值為 false 。
注意這裡完全沒提到JVM啊Class物件啊啥的。另外要注意 instanceof 運算子除了執行時語義外還有部分編譯時限制,詳細參考規範。

如果這樣回答被面試官說“這不是廢話嘛”,請見情形2。

---------------------------------------------------------------------

情形2:
你在面月薪6000-8000的Java研發職位。面試官也知道JVM這麼個大體概念,但知道的也不多。JVM這個概念本身就是“底層”。JVM有一條名為 instanceof 的指令,而Java原始碼編譯到Class檔案時會把Java語言中的 instanceof 運算子對映到JVM的 instanceof 指令上。

你可以知道Java的原始碼編譯器之一javac是這樣做的:
  1. instanceof 是javac能識別的一個關鍵字,對應到Token.INSTANCEOF的token型別。做詞法分析的時候掃描到"instanceof"關鍵字就對映到了一個Token.INSTANCEOF token。hg.openjdk.java.net/jdk
  2. 該編譯器的抽象語法樹節點有一個JCTree.JCInstanceOf類用於表示instanceof運算。做語法分析的時候解析到instanceof運算子就會生成這個JCTree.JCInstanceof型別的節點。hg.openjdk.java.net/jdk term2Rest()
  3. 中途還得根據Java語言規範對instanceof運算子的編譯時檢查的規定把有問題的情況找出來。
  4. 到最後生成位元組碼的時候為JCTree.JCInstanceof節點生成instanceof位元組碼指令。hg.openjdk.java.net/jdk visitTypeTest()

(Java語言君說:“instanceof 這問題直接交給JVM君啦”)
(面試官:你還給我廢話⋯給我進情形3!)

其實能回答到這層面就已經能解決好些實際問題了,例如說需要手工通過位元組碼增強來實現一些功能的話,知道JVM有這麼條 instanceof 指令或許正好就能讓你順利的使用 ASM 之類的庫完成工作。

---------------------------------------------------------------------

情形3:
你在面月薪10000的Java高階研發職位。面試官對JVM有一些瞭解,想讓你說說JVM會如何實現 instanceof 指令。但他可能也沒看過實際的JVM是怎麼做的,只是臆想過一下而已。JVM的規定就是“底層”。這種情況就給他JVM規範對 instanceof 指令的定義就好:
Chapter 6. The Java Virtual Machine Instruction Set, JVM規範Java SE 7版
根據規範來臆想一下實現就能八九不離十的混過這題了。

該層面的答案就照@敖琪前面給出的就差不多了,這邊不再重複。

---------------------------------------------------------------------

情形4:
你可能在面真的簡易JVM的研發職位,或許是啥嵌入式JVM的實現。面試官會希望你對簡易JVM的實現有所瞭解。JVM的直觀實現就是“底層”。這個基本上跟情形3差不多,因為簡易JVM通常會用很直觀的方式去實現。但對具體VM實現得答對一些小細節,例如說這個JVM是如何管理型別資訊的。

這個情形的話下面舉點例子來講講。

---------------------------------------------------------------------

情形5:
你在面試月薪10000以上的Java資深研發職位,注重效能調優啥的。這種職位雖然不直接涉及JVM的研發,但由於效能問題經常源自“抽象洩漏”,對實際使用的JVM的實現的思路需要有所瞭解。面試官對JVM的瞭解可能也就在此程度。對付這個可以用一篇論文:Fast subtype checking in the HotSpot JVM。之前有個討論帖裡討論過對這篇論文的解讀:請教一個share/vm/oops下的程式碼做fast subtype check的問題

---------------------------------------------------------------------

情形6:
你在面試真的高效能JVM的研發職位,例如 HotSpot VM 的研發。JVM在實際桌面或伺服器環境中的具體實現是“底層”。呵呵這要回答起來就複雜了,必須回答出JVM實現中可能做的優化具體的實現。另外找地方詳細寫。

---------------------------------------------------------------------

我覺得會問這種問題的還是情形1和2的比例比較大,換句話說面試官也不知道真的JVM是如何實現這instanceof指令的,可能甚至連這指令的準確語義都無法描述對。那隨便忽悠忽悠就好啦不用太認真。說不定他期待的答案本身就霧很大(逃

碰上情形4、6的話,沒有忽悠的餘地,是怎樣就得怎樣。
情形5可能還稍微有點忽悠餘地呃呵呵。

==============================================================

看倆實際存在的簡易JVM的實現,Kaffe和JamVM。它們都以直譯器為主,JIT的實現非常簡單,主要功能還是在VM runtime裡實現,所以方便考察。
主要考察的是:它們中Java物件的基本結構(如何找到型別資訊),型別資訊自身如何記錄(內部用的型別資訊與Java層的java.lang.Class物件的關係),以及instanceof具體是怎樣實現的。

---------------------------------------------------------------------

Kaffe

github.com/kaffe/kaffe/
Kaffe中Java物件由Hjava_lang_Object結構體表示,裡面有個struct _dispatchTable*型別的欄位vtable,下面再說。

github.com/kaffe/kaffe/
Java層的java.lang.Class例項在VM裡由Hjava_lang_Class結構體表示。Kaffe直接使用Hjava_lang_Class來記錄VM內部的型別資訊。也就是說在Kaffe上執行的Java程式裡持有的java.lang.Class的例項就是該JVM內部存型別資訊的物件。
前面提到的_dispatchTable結構體也在該檔案裡定義。它是一個虛方法分派表,主要用於高效實現invokevirtual。
假如有Hjava_lang_Object* obj,要找到它對應的型別資訊只要這樣:
obj->vtable->class

github.com/kaffe/kaffe/
instanceof的功能由soft.c的soft_instanceof()函式實現。該函式所呼叫的函式大部分都在這個檔案裡。

github.com/kaffe/kaffe/
這邊定義了softcall_instanceof巨集用於在直譯器或者JIT編譯後的程式碼裡呼叫soft_instanceof()函式

github.com/kaffe/kaffe/
這邊定義了instanceof位元組碼指令的處理要呼叫softcall_instanceof巨集

---------------------------------------------------------------------

JamVM

jamvm.cvs.sourceforge.net
JamVM中Java物件由Object結構體表示,Java層的java.lang.Class例項在VM裡由Class表示(是個空Object),VM內部記錄的類資訊由ClassBlock結構體表示(型別名、成員、父類、實現的介面、類價值器啥的都記錄在ClassBlock裡)。比較特別的是每個Class與對應的ClassBlock實際上是粘在一起分配的,所以Class*與ClassBlock*可以很直接的相互轉換。例如說如果有Class* c想拿到它對應的ClassBlock,只要:
ClassBlock* cb = CLASS_CB(c);
即可。
Object結構體裡有Class*型別的成員class,用於記錄物件的型別。

jamvm.cvs.sourceforge.net
instanceof的功能由cast.c第68行的isInstanceOf()函式實現。該函式所呼叫的函式大部分都在這個檔案裡。
jamvm.cvs.sourceforge.net
直譯器主迴圈的程式碼主要在interp.c裡。把instanceof指令的引數所指定的常量池索引解析為實際類指標的邏輯在OPC_INSTANCEOF的實現裡。JamVM做了個優化,在解析好類之後會把instanceof位元組碼改寫為內部位元組碼instanceof_quick;呼叫isInstanceOf()的地方在2161行OPC_INSTANCEOF_QUICK的實現裡,可以看到它呼叫的是isInstanceOf(class, obj->class)。

---------------------------------------------------------------------

上面介紹了Kaffe與JamVM裡instanceof位元組碼的實現相關的程式碼在哪裡。接下來簡單分析下它們的實現策略。

兩者的實現策略其實幾乎一樣,基本上按照下面的步驟:
(假設要檢查的物件引用是obj,目標的型別物件是T)
  1. obj如果為null,則返回false;否則設S為obj的型別物件,剩下的問題就是檢查S是否為T的子型別
  2. 如果S == T,則返回true;
  3. 接下來分為3種情況,S是陣列型別、介面型別或者類型別。之所以要分情況是因為instanceof要做的是“子型別檢查”,而Java語言的型別系統裡陣列型別、介面型別與普通類型別三者的子型別規定都不一樣,必須分開來討論。到這裡雖然例中兩個JVM的具體實現有點區別,但概念上都與JVM規範所描述的 instanceof的基本演算法 幾乎一樣。其中一個細節是:對介面型別的instanceof就直接遍歷S裡記錄的它所實現的介面,看有沒有跟T一致的;而對類型別的instanceof則是遍歷S的super鏈(繼承鏈)一直到Object,看有沒有跟T一致的。遍歷類的super鏈意味著這個演算法的效能會受類的繼承深度的影響。

關於Java語言裡子型別關係的定義,請參考:Chapter 4. Types, Values, and Variables
類型別和介面型別的子型別關係大家可能比較熟悉,而陣列型別的子型別關係可能會讓大家有點意外。
4.10.3. Subtyping among Array Types

The following rules define the direct supertype relation among array types:


  • If S and T are both reference types, then S[] >1 T[] iff S >1 T.

  • Object >1 Object[]

  • Cloneable >1 Object[]

  • java.io.Serializable >1 Object[]

  • If P is a primitive type, then:


    • Object >1 P[]

    • Cloneable >1 P[]

    • java.io.Serializable >1 P[]

這裡稍微舉幾個例子。以下子型別關係都成立(“<:”符號表示左邊是右邊的子型別,“=>”符號表示“推匯出”):
  1. String[][][] <: String[][][] (陣列子型別關係的自反性)
  2. String <: CharSequence => String[] <: CharSequence[] (陣列的協變)
  3. String[][][] <: Object (所有陣列型別是Object的子型別)
  4. int[] <: Serializable (原始型別陣列實現java.io.Serializable介面)
  5. Object[] <: Serializable (引用型別陣列實現java.io.Serializable介面)
  6. int[][][] <: Serializable[][] <: Serializable[] <: Serializable (上面幾個例子的延伸⋯開始好玩了吧?)
  7. int[][][] <: Object[][] <: Object[] <: Object
好玩不?實際JVM在記錄型別資訊的時候必須想辦法把這些相關型別都串起來以便查詢。

另外補充一點:樓主可能會覺得很困惑為啥說到這裡隻字未提ClassLoader——因為在這個問題裡還輪不到它出場。
在一個JVM例項裡,"(型別的全限定名, defining class loader)"這個二元組才可以唯一確定一個類。如果有兩個類全限定名相同,也載入自同一個Class檔案,但defining class loader不同,從VM的角度看它們就是倆不同的類,而且相互沒有子型別關係。instanceof運算子只關心“是否滿足子型別關係”,至於型別名是否相同之類的不需要關心。

通過Kaffe與JamVM兩個例子我們可以看到簡單的JVM實現很多地方就是把JVM規範直觀的實現了出來。這就解決了前面提到的情形4的需求。

==============================================================

至於情形5、6,細節講解起來稍麻煩所以這裡不想展開寫。高效能的JVM跟簡易JVM在細節上完全不是一回事。

簡單來說,優化的主要思路就是把Java語言的特點考慮進來:由於Java的類所繼承的超類與所實現的介面都不會在執行時改變,整個繼承結構是穩定的,某個型別C在繼承結構裡的“深度”是固定不變的。也就是說從某個類出發遍歷它的super鏈,總是會遍歷到不變的內容。
這樣我們就可以把原本要迴圈遍歷super鏈才可以找到的資訊快取在陣列裡,並且以特定的下標從這個陣列找到我們要的資訊。同時,Java的類繼承深度通常不會很深,所以為這個快取陣列選定一個固定的長度就足以優化大部分需要做子型別判斷的情況。

HotSpot VM具體使用了長度為8的快取陣列,記錄某個類從繼承深度0到7的超類。HotSpot把類繼承深度在7以內的超類叫做“主要超型別”(primary super),把所有其它超型別(介面、陣列相關以及超過深度7的超類)叫做“次要超型別”(secondary super)。
對“主要超型別”的子型別判斷不需要像Kaffe或JamVM那樣沿著super鏈做遍歷,而是直接就能判斷子型別關係是否成立。這樣,類的繼承深度對HotSpot VM做子型別判斷的效能影響就變得很小了。
對“次要超型別”,則是讓每個型別把自己的“次要超型別”混在一起記錄在一個陣列裡,要檢查的時候就線性遍歷這個陣列。留意到這裡把介面型別、陣列型別之類的子型別關係都直接記錄在同一個陣列裡了,只要在最初初始化secondary_supers陣列時就分情況填好了,而不用像Kaffe、JamVM那樣每次做instanceof運算時都分開處理這些情況。

舉例來說,如果有下述類繼承關係:
Apple <: Fruit <: Plant <: Object
並且以Object為繼承深度0,那麼對於Apple類來說,它的主要超型別就有:
0: Object
1: Plant
2: Fruit
3: Apple
這個資訊就直接記錄在Apple類的primary_supers陣列裡了。Fruit、Plant等類同理。

如果我們有這樣的程式碼:
Object f = new Apple();
boolean result = f instanceof Plant;

也就是變數f實際指向一個Apple例項,而我們要問這個物件是否是Plant的例項。
可以知道f的實際型別是Apple;要測試的Plant類的繼承深度是1,拿Apple類裡繼承深度為1的主要超型別來看是Plant,馬上就能得出結論是true。
這樣就不需要順著Apple的繼承鏈遍歷過去一個個去看是否跟Plant相等了。

對此感興趣的同學請參考前面在情形5提到的兩個連結。先讀第一個連結那篇論文,然後看第二個連結裡的討論(沒有ACM帳號無法從第一個連結下載到論文的同學可以在第二個連結裡找到一個映象)。

JDK6至今的HotSpot VM實際採用的演算法是:
S.is_subtype_of(T) := {
  int off = T.offset;
  if (S == T) return true;
  if (T == S[off]) return true;
  if (off != &cache) return false;
  if ( S.scan_secondary_subtype_array(T) ) {
    S.cache = T;
    return true;
  }
  return false;
}
(具體是什麼意思請務必參考論文)

這邊想特別強調的一點是:那篇論文描述了HotSpot VM做子型別判斷的演算法,但其實只有HotSpot VM的直譯器以及 java.lang.Class.isInstance() 的呼叫是真的完整按照那個演算法來執行的。HotSpot VM的兩個編譯器,Client Compiler (C1) 與 Server Compiler (C2) 各自對子型別判斷的實現有更進一步的優化。實際上在這個JVM裡,instanceof的功能就實現了4份,VM runtime、直譯器、C1、C2各一份。

VM runtime的:
hg.openjdk.java.net/jdk oopDesc::is_a()
hg.openjdk.java.net/jdk is_subtype_of()
hg.openjdk.java.net/jdk Klass::search_secondary_supers()
inline bool oopDesc::is_a(klassOop k)        const { return blueprint()->is_subtype_of(k); }

  bool is_subtype_of(klassOop k) const {
    juint    off = k->klass_part()->super_check_offset();
    klassOop sup = *(klassOop*)( (address)as_klassOop() + off );
    const juint secondary_offset = in_bytes(secondary_super_cache_offset());
    if (sup == k) {
      return true;
    } else if (off != secondary_offset) {
      return false;
    } else {
      return search_secondary_supers(k);
    }
  }
  bool search_secondary_supers(klassOop k) const;

bool Klass::search_secondary_supers(klassOop k) const {
  // Put some extra logic here out-of-line, before the search proper.
  // This cuts down the size of the inline method.

  // This is necessary, since I am never in my own secondary_super list.
  if (this->as_klassOop() == k)
    return true;
  // Scan the array-of-objects for a match
  int cnt = secondary_supers()->length();
  for (int i = 0; i < cnt; i++) {
    if (secondary_supers()->obj_at(i) == k) {
      ((Klass*)this)->set_secondary_super_cache(k);
      return true;
    }
  }
  return false;
}

直譯器的(以x86-64的template interpreter為例):
hg.openjdk.java.net/jdk TemplateTable::instanceof()
hg.openjdk.java.net/jdk InterpreterMacroAssembler::gen_subtype_check()
(太長,不把程式碼貼出來了。要看程式碼請點上面連結)

C1和C2對instanceof的優化分散在好幾個地方,以C2為例,
最初處理instanceof位元組碼生成C2的內部節點的邏輯主要在:
hg.openjdk.java.net/jdk GraphKit::gen_instanceof()
它會呼叫 GraphKit::gen_subtype_check() 來生成檢查邏輯的主體,而後者會根據程式碼的上下文所能推匯出來的型別資訊把型別檢查儘量優化到更簡單的形式,甚至直接就得出結論。

對這部分細節感興趣的同學請單獨聯絡我或者另外開帖討論吧。

下面兩個patch是我對HotSpot VM在子型別檢查相關方面做的小優化,兩個都在JDK7u40/JDK8裡釋出:

[#JDK-7170463] C2 should recognize "obj.getClass() == A.class" code pattern
Request for review (S): C2 should recognize "obj.getClass() == A.class" code pattern
hg.openjdk.java.net/hsx

[#JDK-7171890] C1: add Class.isInstance intrinsic
Request for review (M): 7171890: C1: add Class.isInstance intrinsic
hg.openjdk.java.net/hsx
hg.openjdk.java.net/lam

舉個例子,經過JDK-7170463的patch之後,HotSpot VM的C2會把下面這樣的程式碼:
if (obj.getClass() == A.class) {
  boolean isInst = obj instanceof A;
}
優化為:
if (obj.getClass() == A.class) {
  boolean isInst = true;
}
那個instanceof運算就直接被常量摺疊掉了。樓主可以看看,當時面試你的面試官是否瞭解到這種細節了,而他又是否真的要在面試種考察這種細節。

==============================================================

樓主的問題原本有提到 BytecodeInstanceOf.java 。它是 Serviceability Agent 的一部分,不是 HotSpot VM 內的邏輯。關於 Serviceability Agent 請從這帖裡的連結找資料來讀讀:記GreenTeaJUG第二次線下活動(杭州)

SA是HotSpot VM自帶的一個用來除錯、診斷HotSpot VM執行狀態的工具。它是一個“程式外”條偵錯程式,也就是說假如我們要除錯的HotSpot VM執行在程式A裡,那麼SA要執行在另一個程式B裡去除錯程式A。這樣做的好處是SA與被除錯程式不會相互干擾,於是除錯就可以更乾淨的進行;就算SA自己崩潰了也(通常)不會連帶把被除錯程式也弄崩潰。

SA在HotSpot VM內部的C++程式碼裡嵌有一小塊,主要是把HotSpot的C++類的符號資訊記錄下來;SA的主體則是用Java來實現的,把可除錯的HotSpot裡C++的類用Java再做一層皮。樓主看到的BytecodeInstanceOf類就是這樣的一層皮,它並不包含HotSpot的執行邏輯,純粹是為除錯用的。

直接放些外部參考資料連結方便大家找:

The HotSpot™ Serviceability Agent: An Out-of-Process High-Level Debugger for a Java™ Virtual Machine, USENIX JVM '01
這篇是描述 HotSpot Serviceability Agent 的原始論文,要了解 SA 的背景必讀。

HotSpot source: Serviceability Agent (A. Sundararajan's Weblog)
提到了hotspot/agent目錄裡的程式碼都是 Serviceability Agent 的實現。注意 SA 並不是 HotSpot VM 執行時必要的組成部分。

Serviceability in HotSpot, OpenJDK
這是OpenJDK館網上的相關文件頁面。

相關文章