1. 程式人生 > >04 JVM是如何執行方法調用的(下)

04 JVM是如何執行方法調用的(下)

操作 靜態 文章 加載過程 mage 空間 sta 實現 整理

虛方法調用

Java 裏所有非私有實例方法調用都會被編譯成 invokevirtual 指令,而接口方法調用會被編譯成 invokeinterface 指令。這兩種指令,均屬於 Java 虛擬機中的虛方法調用。

動態綁定:Java 虛擬機需要根據調用者的動態類型,來確定虛方法調用的目標方法。

靜態綁定:調用靜態方法的 invokestatic 指令,以及用於調用構造器,私有實例方法和超類非私有實例方法的 invokestatic 指令。如果虛方法調用指向一個標記為 final 的方法,那麽 Java 虛擬機也可以靜態綁定該虛方法調用的目標方法。

關於動態綁定,Java 虛擬機采用一種空間換區時間的策略來實現動態綁定。它為每個類生成一張方法表,用以快速定位目標方法。

方法表

類的加載過程中,到了準備階段,除了為靜態字段分配內存之外,還會構造於該類相關聯的方法表。方法表是 Java 虛擬機實現動態綁定的關鍵所在。

下面以 invokevirtual 所使用的虛方法表(virtual method table,vtable)為例介紹方法表的用法。

方法表本質上是一個數組,每個數組元素指向一個當前類及其祖先類中非私有的實例方法。這些方法可以是具體的可執行的方法,也可以是沒有想用字節碼的抽象方法。

方法表滿足兩個特質:一,子類方法表中包含父類方法表中的所有方法;二,子類方法在方法表中的索引值,與它所重寫的父類方法的索引值相同。

類的加載過程中,方法調用指令中的符號引用會在執行之前解析成實際引用。對於靜態綁定的方法調用而言,實際引用將指向具體的目標方法。對於動態綁定的方法調用而言,實際引用則是方法表的索引值(並不僅僅是索引值。)。

在執行過程中,Java 虛擬機將獲取調用者的十幾類型,並在該實際類型的虛方法表中,根據索引值獲得目標方法。這個過程就是動態綁定。

動態綁定與靜態綁定相比,僅僅是多出幾個內存解引用的操作:訪問棧上的調用者,讀取調用者的動態類型,讀取該類型的方法表,讀取方法表中某個索引對應的目標方法。對於創建並初始化 Java 棧幀來說,這幾個內存解引用操作的開銷簡直可以忽略不計。

利用方法表的方式優化虛方法的調用,這種優化僅存在於解釋執行中,或者即時編譯代碼的最壞情況中。即時編譯還有另外兩種性能更好的優化手段:內聯緩存和方法內聯。

內聯緩存

內聯緩存是一種加快動態動態綁定的優化技術。它能夠緩存虛方法調用中調用者的動態類型,以及該類型所對應的目標方法。在之後的執行過程中,如果碰到已緩存的類型,內聯緩存便會直接調用該類型所對應的目標方法。如果沒有緩存,則會退化為基於方法表的動態綁定。

在針對多態的優化手段中,需要了解一下三個術語:
1:單態,指的是僅有一種狀態的情況。
2:多態,指的是有限數量種多態的情況。
3:超多態,指的是更多種狀態的情況。通常用一個具體值區分多態和超態,數值之下為多態,數值之上為超態。

對於內聯緩存,有上述三種對用的內聯緩存。

單態內聯緩存:只緩存一種動態類型以及它所對應的目標方法。

多態內聯緩存:緩存多個動態類型以及它所對應的目標方法。使用的時候,需要遍歷多個緩存數據,如果命中則調用對應的目標方法。在時間操作中,大部分的虛方法調用均是單態的,也就是只用一種動態類型。為了節省內存空間,Java 虛擬機只采用單態的內聯緩存。如果沒有命中緩存,Java 虛擬機就要重新使用方法表進行動態綁定。

這種情況下,對於內聯緩存,我們有兩種選擇。一:替換單態內聯緩存中的記錄。替換後,要保證方法調用者的動態類型與方法調用的類型保持一致,從而能夠有效的利用內聯緩存。最壞的情況是兩種不同的動態類型交替調用,對於內聯緩存就只有寫的額外開銷,並沒有利用緩存來提高性能。二:劣化為超多態狀態。這也是 Java 虛擬機的具體實現方式。這種情況下,其實已經放棄了緩存優化,直接訪問方法表來動態綁定目標方法。於內聯緩存相比,方法表犧牲了優化的機會,但是卻節省了寫內存的額外開銷。

問答

Q:為什麽調用超類非私有實例方法會屬於靜態綁定呢?

通過super關鍵字來調用父類方法,本意就是想要調用父類的特定方法,而不是根據具體類型決定目標方法。

總結

本文創作靈感來源於 極客時間 鄭雨迪老師的《深入拆解 Java 虛擬機》課程,通過課後反思以及借鑒各位學友的發言總結,現整理出自己的知識架構,以便日後溫故知新,查漏補缺。

關註本人公眾號,第一時間獲取最新文章發布,每日更新一篇技術文章。

技術分享圖片

04 JVM是如何執行方法調用的(下)