写点什么

JVM 系列之:JIT 中的 Virtual Call

发布于: 2020 年 08 月 03 日
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 的博客

欢迎关注我的公众号:程序那些事,更多精彩等着您!


发布于: 2020 年 08 月 03 日阅读数: 72
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
JVM系列之:JIT中的Virtual Call