ROXY_FACTORY 就是一个 static 的字段。
上面的代码的字节码大概是这样的:
通过字节码你可以看到,首先有一个 getstatic 调用,来获得静态字段 PROXY_FACTORY 的值。
还有一个 invokevirtual 指令的调用,对应的就是 ProxyFactory 实例的 getProxyPreparedStatement() 方法:
15: invokevirtual #69 // Method com/zaxxer/hikari/proxy/ProxyFactory.getProxyPreparedStatement:(Lcom/zaxxer/hikari/proxy/ConnectionProxy;Ljava/sql/PreparedStatement;)Ljava/sql/PreparedStatement;
这个地方有什么优化空间呢?
作者把代码修改成了这样:
其中 ProxyFactory 是通过 Javassist 生成的。
所以你去看 ProxyFactory 源码,全是空实现,
它真正的实现逻辑,是对应源代码的这个类,就不具体展示了,有兴趣的可以下来看看:
com.zaxxer.hikari.util.JavassistProxyFactory
然后,把 getProxyPreparedStatement
方法做成了 static。
然后字节码就变成了这样:
- getstatic 指令消失了
- invokevirtual 被替换成了 invokestatic 调用,这样更加容易被JVM优化。
- 最后,可能第一眼没有注意到的是,堆栈大小从 5 减少到 4 。这是因为在 invokevirtual 的情况下,ProxyFactory 的实例被隐含地传递到了堆栈中(也就是 this 对象),而且在调用 getProxyPreparedStatement() 时,还有一个额外的从堆栈中弹出的操作。
第 1,3 点应该问题不大。大家都能明白是怎么回事。
但是这个第二点:invokevirtual 被替换成了 invokestatic 调用,这样更加容易被JVM优化。
说真的,我第一次看到的时候大概是这样的:
为啥啊?
invokevirtual 和 invokestatic 是干啥的我倒是还记得。
但是 invokestatic 的性能会更好一点吗?
于是我带着这个问题去翻了《深入理解JVM虚拟机》,没有直接找到答案。
但是还是有意外收获的。就是写下了这篇文章:《报告!书里有个BUG》
不然你觉得我为什么会突然翻到书里面的这一部分,都是有契机的。
虽然,书里面没有直接把答案写出来,但是在相关部分有这样的一段话:
我理解一下就是 invokevirtual 指令,需要查询虚方法表才能确定方法的直接引用。
而 invokestatic 在类加载的时候,就可以从符号引用转成直接引用。
这样看来,invokestatic 确实是优于 invokevirtual 的。
那么问题又来了。
类加载的过程是什么?
加载、验证、准备、解析、初始化。
invokestatic 是在哪个过程搞事情的?
肯定是解析阶段哈,朋友们。
解析阶段,就是 JVM 将常量池内的符号引用替换为直接引用的过程。
扯远了,说回来。
上面只是我的一点猜测,我相信肯定不止我一个人看了作者的“兔子洞”文章后关于 invokevirtual vs invokestatic 这一块有疑问。
于是,我去查了一圈。
果不其特么的然。(抱歉爆粗了,但是我确实找了很久。)
找到了这个链接,链接的前半部分和我的问题一模一样:
后面那一段 Additionally 很好理解。
就是前面说的,静态调用少一个堆栈,在运行时就少一个推/拉操作,这进一步提高了性能。
主要是前面这段,有亿点点难懂。
他说:简而言之,JVM 在做内联调用的时候,即使是单态的内联,它也必须安装一个 trap(陷阱),以防另一个实现出现,并将调用转变为多态。
这个 trap 的设置和清除给调用增加了一点开销。
怎么样,懵不懵逼?
其实,他这句话,我个人理解,说的就是 Java 的动态分派,聊的就是 JVM 的 CHA(Class Hierarchy Analysis,类型继承关系分析) 技术。
答案就写在《深入理解Java虚拟机(第三版)》的 417 页,翻去吧: