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

JVM系列之:JIT中的Virtual Call

[toc] # 簡介 什麼是Virtual Call?Virtual Call在java中的實現是怎麼樣的?Virtual Call在JIT中有沒有優化? 所有的答案看完這篇文章就明白了。 # Virtual Call和它的本質 有用過PrintAssembly的朋友,可能會在反編譯的彙編程式碼中發現有些方法呼叫的說明是invokevirtual,實際上這個invokevirtual就是Virtual Call。 Virtual Call是什麼呢? 面向物件的程式語言基本上都支援方法的重寫,我們考慮下面的情況: ~~~java 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是子類。然後我們通一個方法來呼叫他們: ~~~java public static void doWithVMethod(CustObj obj) { obj.methodCall(); } ~~~ 因為doWithVMethod的引數型別是CustObj,但是我們同樣也可以傳一個CustObj2物件給doWithVMethod。 怎麼傳遞這個引數是在執行時決定的,我們很難在編譯的時候判斷到底該如何執行。 那麼JVM會怎麼處理這個問題呢? 答案就是引入VMT(Virtual Method Table),這個VMT儲存的是該class物件中所有的Virtual Method。 然後class的例項物件儲存著一個VMT的指標,執行VMT。 ![](https://img-blog.csdnimg.cn/20200630084945828.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 程式執行的時候首先載入例項物件,然後通過例項物件找到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程式碼的分析。 要執行的程式碼如下: ~~~java 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!"); } } } } ~~~ 上面的例子中我們只定義了一個類的方法實現。 ![](https://img-blog.csdnimg.cn/2020063009022458.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 在JIT Watcher的配置中,我們禁用inline,以免inline的結果對我們的分析進行干擾。 > 如果你不想使用JIT Watcher,那麼可以在執行是新增引數-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:-Inline, 這裡使用JIT Watcher是為了方便分析。 好了執行程式碼: 執行完畢,介面直接定位到我們的JIT編譯程式碼的部分,如下圖所示: ![](https://img-blog.csdnimg.cn/20200630090523130.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) obj.methodCall相對應的byteCode中,大家可以看到第二行就是invokevirtual,和它對應的彙編程式碼我也在最右邊標明瞭。 大家可以看到在invokevirtual methodCall的最下面,已經寫明瞭optimized virtual_call,表示這個方法已經被JIT優化過了。 接下來,我們開啟inline選項,再執行一次: ![](https://img-blog.csdnimg.cn/20200630091406865.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 大家可以看到methodCall中的System.currentTimeMillis已經被內聯到methodCall中了。 因為內聯只會發生在classic calls中,所以也側面說明了methodCall方法已經被優化了。 # Virtual Call優化多實現方法的例子 上面我們講了一個方法的實現,現在我們測試一下兩個方法的實現: ~~~java 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。 ![](https://img-blog.csdnimg.cn/20200630091910897.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 大家可以看到結果中,首先對兩個物件做了cmp,然後出現了兩個優化過的virtual call。 這裡比較的作用就是找到兩個例項物件中的方法地址,從而進行優化。 那麼問題來了,兩個物件可以優化,三個物件,四個物件呢? 我們選擇三個物件來進行分析: ~~~java 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!"); } } } } ~~~ 執行程式碼,結果如下: ![](https://img-blog.csdnimg.cn/20200630092508545.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 很遺憾,程式碼並沒有進行優化。 >
具體未進行優化的原因我也不清楚,猜想可能跟code cache的大小有關? 有知道的朋友可以告訴我。 # 總結 本文介紹了Virtual Call和它在java程式碼中的使用,並在組合語言的角度對其進行了一定程度的分析,有不對的地方還請大家不吝指教! > 本文作者:flydean程式那些事 > > 本文連結:[http://www.flydean.com/jvm-virtual-call/](http://www.flydean.com/jvm-virtual-call/) > > 本文來源:flydean的部落格 > > 歡迎關注我的公眾號:程式那些事,更多精彩等