JVM系列之:JIT中的Virtual Call
阿新 • • 發佈:2020-08-03
[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的部落格
>
> 歡迎關注我的公眾號:程式那些事,更多精彩等