invokestatic性能比invokevirtual好?

JAVA herman 176浏览
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:codedq,发送下载链接帮助你免费下载!
本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:codedq,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领

你好,我是业余草,这是我的第 445 篇原创文章。

这篇文章,我想了很久,没想到好名字。所以就随便起了一个,和群友保持一致!

这两天看到群里有人阅读到网上的文章,在群里问:“invokestatic 性能比 invokevirtual 好?”

一时间难倒了不少人,有人建议去看周老师的 JVM 书籍(深入理解Java虚拟机)中找答案,引起了群友广泛的讨论。不少人表示没看过,还有部分表示看不懂。

其实不看周老师的书,也能搞定这个问题。

同时,阿里巴巴出品的 Java 开发手册中也有一段这样的描述:

【强制】避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。

https://github.com/alibaba/p3c/blob/master/Java%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C%EF%BC%88%E5%B5%A9%E5%B1%B1%E7%89%88%EF%BC%89.pdf

这句话说的太模糊了,也不给一个详细的解释,看的云里雾里。很多人都看懵了,也包括我自己。没得办法,我尝试着从字节码的角度找找答案,结果还真被我找到了。

我们先来看一个 demo:

public class Xttblog {
    public static int num = 0;

    public void add(){
        this.num ++;
    }
}

这段代码,如果你安装了 p3c 插件,就会有爆红提示:

不应该通过类实例访问静态成员
不应该通过类实例访问静态成员

我们先忽略这个提示,写个 main 方法,看看字节码层面指令。

public class StaticTest {
    public static void main(String[] args) {
        Xttblog xttblog = new Xttblog();
        xttblog.num ++;
    }
}

通过类实例访问静态变量,阿里巴巴的规约插件同样的会报出红线提示。

阿里巴巴Java开发规约插件p3c提示
阿里巴巴Java开发规约插件p3c提示

这个提示不影响代码运行。我们可以执行javap,不会用的先看java -help,或者 idea 安装字节码插件:jclasslib。

Compiled from "StaticTest.java"
public class com.xttblog.test.StaticTest {
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/xttblog/Xttblog
         3: dup
         4: invokespecial #3                  // Method com/xttblog/Xttblog."<init>":()V
         7: astore_1
         8: aload_1
         9: pop
        10: getstatic     #4                  // Field com/xttblog/Xttblog.num:I
        13: iconst_1
        14: iadd
        15: putstatic     #4                  // Field com/xttblog/Xttblog.num:I
        18: return
              LineNumberTable:
        line 11: 0
        line 12: 8
        line 13: 18
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      19     0  args   [Ljava/lang/String;
            8      11     1 xttblog   Lcom/xttblog/Xttblog;
}

我们再次更新一下 main 方法中的代码:

public class StaticTest {
    public static void main(String[] args) {
        /*Xttblog xttblog = new Xttblog();
        xttblog.num++; */

        Xttblog.num ++;
    }
}

再一次的查看字节码。

Compiled from "StaticTest.java"
public class com.xttblog.test.StaticTest {
  public com.xttblog.test.StaticTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field com/xttblog/test/Xttblog.num:I
       3: iconst_1
       4: iadd
       5: putstatic     #2                  // Field com/xttblog/test/Xttblog.num:I
       8: return

你会发现指令变少了。除了少了创建对象的 new、dup、invokespecial 指令外,还多出了 astore_1、aload_1、pop 指令。

astore_1 指令:1 是代表在本地变量表(LocalVariableTable)中的序号位置。Slot 1 就是 xttblog 对象。它的意思就是,创建完对象后,把 xttblog 存入到本地变量表位置 1 中。

aload_1 指令:把存放在局部变量表中索引 1 位置的对象引用压入操作栈。

pop 指令:然后,pop 指令,又把栈顶给弹出了。

到现在,你应该看明白了。通过类实例去访问静态变量,来来回回的把 xttblog 对象给折腾了一遍。它多出了,存入本地变量表,压栈,出栈的操作。把本来利索的事情,给多加几道工序,反而影响运行效率。

其他的指令我就不讲了,具体可以查看我前面的文章:《JVM 常用指令速查手册,建议收藏!》。

JVM部分指令
JVM部分指令

总结一下,就是静态变量或方法的访问比通过类实例访问静态变量和方法快。同样的道理,invokestatic 性能比 invokevirtual 好,也就不足为奇了。

实际上,HikariCP 也利用了 Javassist 来生成委托实现动态代理,优化并精简了字节码。

invokevirtual vs invokestatic
invokevirtual vs invokestatic

具体官方英文文档,参考这里:https://github.com/brettwooldridge/HikariCP/wiki/Down-the-Rabbit-Hole

HikariCP 的作者,也就是 Brett Wooldridge 大神也是通过字节码来说明,invokestatic 性能比 invokevirtual 好,同时 invokestatic 相比 invokevirtual 更容易被 JVM 优化调用。

HikariCP作者的性能优化
HikariCP作者的性能优化

另外,优化后的(上图中的上半图)指令,还少了一个 getstatic。同时栈大小从 5 个元素减少到 4 个元素。这是因为 invokevirtual 指令在堆栈上隐式传递 ProxyFactory 实例的情况下(即 this),并且在调用时从堆栈中需要额外(看不见)弹出 this。

读过周志明老师的《深入理解Java虚拟机》第二版的同学,可能还知道:invokevirtual 指令的调用依赖于运行时解析,其解析过程大致分为以下几个步骤:

  • 找到操作数栈顶的第一个元素(本例中就是 this)所指向的对象的实际类型,记作 C。
  • 如果在类型 C 中找到了与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常。
  • 否则,按照继承关系对 C 的父类进行第 2 步的搜索和校验过程。
  • 如果始终都没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常。

说白了,invokevirtual 会进行查找,查找重载函数,以及权限验证。找出类型和权限全符合的函数进行执行。

而 invokestatic 指令用于调用静态方法,即使用 static 关键字修饰的方法;static 修饰的方法是属于具体类的,因此不需要多余的查找和权限验证,因此 invokestatic 的性能比 invokevirtual 好!

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号1:xmtxtt(5000人已满),微信号2:xttblog(5000人已满),微信号3:codedq(超3800)。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!

本文原文出处:业余草: » invokestatic性能比invokevirtual好?