1. 程式人生 > 實用技巧 >JVM系列之:JIT中的Virtual Call

JVM系列之:JIT中的Virtual Call

目錄

簡介

什麼是Virtual Call?Virtual Call在java中的實現是怎麼樣的?Virtual Call在JIT中有沒有優化?

所有的答案看完這篇文章就明白了。

Virtual Call和它的本質

有用過PrintAssembly的朋友,可能會在反編譯的彙編程式碼中發現有些方法呼叫的說明是invokevirtual,實際上這個invokevirtual就是Virtual Call。

Virtual Call是什麼呢?

面向物件的程式語言基本上都支援方法的重寫,我們考慮下面的情況:

 private static class CustObj
    {
        public void methodCall()
        {
            if(System.currentTimeMillis()== 0){
                System.out.println("CustObj is very good!");
            }
        }
    }
    private static class CustObj2 extends  CustObj
    {
        public final void methodCall()
        {
            if(System.currentTimeMillis()== 0){
                System.out.println("CustObj2 is very good!");
            }
        }
    }

我們定義了兩個類,CustObj是父類CustObj2是子類。然後我們通一個方法來呼叫他們:

public static void doWithVMethod(CustObj obj)
    {
        obj.methodCall();
    }

因為doWithVMethod的引數型別是CustObj,但是我們同樣也可以傳一個CustObj2物件給doWithVMethod。

怎麼傳遞這個引數是在執行時決定的,我們很難在編譯的時候判斷到底該如何執行。

那麼JVM會怎麼處理這個問題呢?

答案就是引入VMT(Virtual Method Table),這個VMT儲存的是該class物件中所有的Virtual Method。

然後class的例項物件儲存著一個VMT的指標,執行VMT。

程式執行的時候首先載入例項物件,然後通過例項物件找到VMT,通過VMT再找到對應的方法地址。

Virtual Call和classic call

Virtual Call意思是呼叫方法的時候需要依賴不同的例項物件。而classic call就是直接指向方法的地址,而不需要通過VMT表的轉換。

所以classic call通常會比Virtual Call要快。

那麼在java中是什麼情況呢?

在java中除了static, private和建構函式之外,其他的預設都是Virtual Call。

Virtual Call優化單實現方法的例子

有些朋友可能會有疑問了,java中其他方法預設都是Virtual Call,那麼如果只有一個方法的實現,效能不會受影響嗎?

不用怕,JIT足夠智慧,可以檢測到這種情況,在這種情況下JIT會對Virtual Call進行優化。

接下來,我們使用JIT Watcher來進行Assembly程式碼的分析。

要執行的程式碼如下:

public class TestVirtualCall {

    public static void main(String[] args) throws InterruptedException {
        CustObj obj = new CustObj();
        for (int i = 0; i < 10000; i++)
        {
            doWithVMethod(obj);
        }
        Thread.sleep(1000);
    }

    public static void doWithVMethod(CustObj obj)
    {
        obj.methodCall();
    }

    private static class CustObj
    {
        public void methodCall()
        {
            if(System.currentTimeMillis()== 0){
                System.out.println("CustObj is very good!");
            }
        }
    }
}

上面的例子中我們只定義了一個類的方法實現。

在JIT Watcher的配置中,我們禁用inline,以免inline的結果對我們的分析進行干擾。

如果你不想使用JIT Watcher,那麼可以在執行是新增引數-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:-Inline, 這裡使用JIT Watcher是為了方便分析。

好了執行程式碼:

執行完畢,介面直接定位到我們的JIT編譯程式碼的部分,如下圖所示:

obj.methodCall相對應的byteCode中,大家可以看到第二行就是invokevirtual,和它對應的彙編程式碼我也在最右邊標明瞭。

大家可以看到在invokevirtual methodCall的最下面,已經寫明瞭optimized virtual_call,表示這個方法已經被JIT優化過了。

接下來,我們開啟inline選項,再執行一次:

大家可以看到methodCall中的System.currentTimeMillis已經被內聯到methodCall中了。

因為內聯只會發生在classic calls中,所以也側面說明了methodCall方法已經被優化了。

Virtual Call優化多實現方法的例子

上面我們講了一個方法的實現,現在我們測試一下兩個方法的實現:

public class TestVirtualCall2 {

    public static void main(String[] args) throws InterruptedException {
        CustObj obj = new CustObj();
        CustObj2 obj2 = new CustObj2();
        for (int i = 0; i < 10000; i++)
        {
            doWithVMethod(obj);
            doWithVMethod(obj2);

        }
        Thread.sleep(1000);
    }

    public static void doWithVMethod(CustObj obj)
    {
        obj.methodCall();
    }

    private static class CustObj
    {
        public void methodCall()
        {
            if(System.currentTimeMillis()== 0){
                System.out.println("CustObj is very good!");
            }
        }
    }
    private static class CustObj2 extends  CustObj
    {
        public final void methodCall()
        {
            if(System.currentTimeMillis()== 0){
                System.out.println("CustObj2 is very good!");
            }
        }
    }
}

上面的例子中我們定義了兩個類CustObj和CustObj2。

再次執行看下結果,同樣的,我們還是禁用inline。

大家可以看到結果中,首先對兩個物件做了cmp,然後出現了兩個優化過的virtual call。

這裡比較的作用就是找到兩個例項物件中的方法地址,從而進行優化。

那麼問題來了,兩個物件可以優化,三個物件,四個物件呢?

我們選擇三個物件來進行分析:

public class TestVirtualCall4 {

    public static void main(String[] args) throws InterruptedException {
        CustObj obj = new CustObj();
        CustObj2 obj2 = new CustObj2();
        CustObj3 obj3 = new CustObj3();
        for (int i = 0; i < 10000; i++)
        {
            doWithVMethod(obj);
            doWithVMethod(obj2);
            doWithVMethod(obj3);

        }
        Thread.sleep(1000);
    }

    public static void doWithVMethod(CustObj obj)
    {
        obj.methodCall();
    }

    private static class CustObj
    {
        public void methodCall()
        {
            if(System.currentTimeMillis()== 0){
                System.out.println("CustObj is very good!");
            }
        }
    }
    private static class CustObj2 extends  CustObj
    {
        public final void methodCall()
        {
            if(System.currentTimeMillis()== 0){
                System.out.println("CustObj2 is very good!");
            }
        }
    }
    private static class CustObj3 extends  CustObj
    {
        public final void methodCall()
        {
            if(System.currentTimeMillis()== 0){
                System.out.println("CustObj3 is very good!");
            }
        }
    }
}

執行程式碼,結果如下:

很遺憾,程式碼並沒有進行優化。

具體未進行優化的原因我也不清楚,猜想可能跟code cache的大小有關? 有知道的朋友可以告訴我。

總結

本文介紹了Virtual Call和它在java程式碼中的使用,並在組合語言的角度對其進行了一定程度的分析,有不對的地方還請大家不吝指教!

本文作者:flydean程式那些事

本文連結:http://www.flydean.com/jvm-virtual-call/

本文來源:flydean的部落格

歡迎關注我的公眾號:程式那些事,更多精彩等著您!