2010-04-01 20 views
11

考慮這個簡單的Java類:爲什麼Java的invokevirtual需要解析被調用方法的編譯時類?

class MyClass { 
    public void bar(MyClass c) { 
    c.foo(); 
    } 
} 

我想討論就行c.foo會發生什麼()。

原始記載,誤導性問題

注:並非所有的這實際上與每個獨立invokevirtual操作碼發生。提示:如果您想了解Java方法調用,請不要閱讀invokevirtual的文檔!

在字節碼級,c.foo的肉()將是invokevirtual操作碼,並根據the documentation for invokevirtual,更多或更少的將發生以下情況:

  1. 查找所定義的方法foo的在編譯時間 class MyClass。 (這涉及到首先解析MyClass。)
  2. 執行一些檢查,包括:驗證c不是初始化方法,並確認調用MyClass.foo不會違反任何受保護的修飾符。
  3. 找出實際調用的方法。特別是查看c的運行時間類型。如果該類型具有foo(),則調用該方法並返回。如果不是,查找c的運行時類型的超類;如果該類型具有foo,則調用該方法並返回。如果沒有,請查看c的運行時類型的超類的超類;如果該類型具有foo,則調用該方法並返回。等..如果沒有找到合適的方法,那麼錯誤。

僅僅步驟#3似乎足以找出調用哪種方法並驗證所述方法是否具有正確的參數/返回類型。所以我的問題是爲什麼第一步得到執行。可能的答案似乎爲:

  • 在步驟#1完成之前,您沒有足夠的信息來執行步驟#3。 (這似乎乍看起來難以置信,所以請解釋一下。)
  • 在#1和#2中完成的鏈接或訪問修飾符檢查對防止發生某些不好的事情是必不可少的,這些檢查必須基於編譯時類型,而不是運行時類型層次結構。 (請說明。)

經修訂的課題

javac編譯器輸出爲線c.foo(的核心)會是這樣的指令:

invokevirtual i 

其中i是MyClass的運行時常量池的索引。該常量池條目的類型爲CONSTANT_Methodref_info,並且將指示(可能是間接的)A)所調用方法的名稱(即foo),B)方法簽名,以及C)稱爲該方法的編譯時間類的名稱(即MyClass)。

問題是,爲什麼需要編譯時類型(MyClass)的引用?由於invokevirtual將對c的運行時類型執行動態分派,因此將引用存儲到編譯時類不是多餘的嗎?

+0

這是由於驗證。見下面我更新的答案。 – 2010-04-02 11:54:04

回答

4

這完全是關於性能。通過計算編譯時類型(又名:靜態類型),JVM可以計算運行時類型的虛擬函數表(又名:動態類型)中被調用方法的索引。使用這個索引步驟3簡單地成爲一個可以在恆定時間內完成的數組訪問。不需要循環。

實施例:

class A { 
    void foo() { } 
    void bar() { } 
} 

class B extends A { 
    void foo() { } // Overrides A.foo() 
} 

默認情況下,A延伸Object限定這些方法(省略,因爲它們是經由invokespecial調用最終方法):

class Object { 
    public int hashCode() { ... } 
    public boolean equals(Object o) { ... } 
    public String toString() { ... } 
    protected void finalize() { ... } 
    protected Object clone() { ... } 
} 

現在,考慮這個調用:

A x = ...; 
x.foo(); 

通過計算出th在x的靜態類型是A JVM還可以找出在此調用站點可用的方法列表:hashCode,equals, toString, finalize, clone, foo, bar。在此列表中,foo是第6個條目(hashCode是第1個,equals是第2個等)。該索引的計算僅執行一次 - 當JVM加載類文件時。

在那之後,每當JVM進程x.foo()只是需要訪問的是X提供方法列表中的第六項,相當於x.getClass().getMethods[5],(這點在A.foo()如果x的動態類型爲A),並調用該方法。不需要詳盡地搜索這個數組的方法。

請注意,無論x的動態類型如何,該方法的索引都保持不變。即:即使x指向B的一個實例,第6個方法仍然是foo(儘管這次它指向B.foo())。

更新

[在你更新的光]:你說的沒錯。爲了執行虛擬方法調度,所有JVM需求都是方法的名稱+簽名(或vtable中的偏移量)。但是,JVM不會盲目執行。它首先檢查在名爲verification的過程中裝入的cassfiles是否正確(另請參閱here)。

驗證表示JVM的設計原理之一:它不依賴編譯器來生成正確的代碼。它在它允許執行之前檢查代碼本身。具體來說,驗證器檢查每個調用的虛擬方法實際上是由接收器對象的靜態類型定義的。顯然,接收器的靜態類型需要執行這樣的檢查。

+0

一個小的修正是'invokevirtual'調用'final'方法。 – 2013-06-04 20:45:25

1

這不是我在閱讀文檔後理解它的方式。我認爲你已經將步驟2和步驟3轉換了,這將使整個系列的事件更加合乎邏輯。

+0

假設我有步驟2和3轉置。 (這似乎是合理的,在我提到的文檔中,「命名方法已解決」這個句子似乎是不明確的。)您是否仍然同意JVM正在對編譯時類型進行某種檢查,或者您懷疑我也有這個錯誤? (特別是,所有的檢查都是針對運行時類型的?)我仍然非常確定JVM確實知道MyClass是與foo調用相關聯的編譯時類型,即使我對它的作用模糊有了這些信息。 – Chris 2010-04-01 22:18:36

+0

進一步閱讀:) 1)從invokevirtual的操作數計算的索引用於查看MyClass的運行時常量池,它將指向該方法的符號引用。就像:MyClass/foo()V。 2)從該符號引用中查找類「MyClass」,並在該類中查找方法「void foo()」並檢查訪問保護。 3)檢查變量「c」的運行時類型是否存在「void foo()」方法,如果不是,則它會在類層次結構上遞歸,直到找到一個爲止。也許它做了第一步,以便它可以快速失敗。邁克爾E也許是對的;) – 2010-04-01 23:33:06

+0

順便說一句,感謝您提出這個問題 - 非常有教育意義。 – 2010-04-01 23:34:58

1

據推測,#1和#2已經由編譯器發生了。我懷疑至少有一部分目的是爲了確保它們仍然保留運行時環境中的類的版本,這可能與編譯代碼的版本不同。

儘管如此,我還沒有消化invokevirtual文檔來驗證您的摘要,所以Rob Heiser可能是對的。

1

我猜答案「B」。

在#1和#2中完成的鏈接或訪問修飾符檢查對於防止某些不好的事情發生是必不可少的,而且這些檢查必須基於編譯時類型而不是運行時類型層次結構。 (請說明)

#1被描述爲5.4.3.3 Method Resolution,它進行了一些重要的檢查。例如,#1檢查編譯時類型中方法的可訪問性,如果不是,則可能返回IllegalAccessError:

...否則,如果引用的方法不可訪問(第5.4.4節)到D,方法解析會拋出IllegalAccessError。 ...

如果您只檢查運行時類型(通過#3),則運行時類型可能會非法擴大覆蓋方法的可訪問性(又名「壞事」)。確實,編譯器應該阻止這種情況,但是JVM仍然保護自己免受惡意代碼(例如手動構建的惡意代碼)的侵害。

0

要完全理解這個東西,您需要了解Java中的方法解析是如何工作的。如果您正在尋找深入的解釋,我建議您閱讀本書「Java虛擬機內部」。從第8章下面的部分, 「的銜接模式」,可在網上,似乎尤其重要:

(CONSTANT_Methodref_info項是在類的條目描述該類所調用的方法的文件頭。)

感謝itay鼓舞人心的m e做谷歌搜索需要找到這個。

相關問題