从源码里的一个注释,我追溯到了12年前,有点意思。 (上)

简介: 从源码里的一个注释,我追溯到了12年前,有点意思。 (上)

你好呀,我是歪歪。

那天我正在用键盘疯狂的输出:

image.png

突然微信弹出一个消息,是一个读者发给我的。

我点开一看:

image.png

啊,这熟悉的味道,一看就是 HashMap,八股文梦开始的地方啊。

但是他问出的问题,似乎又不是一个属于 HashMap 的八股文:

为什么这里要把 table 变量赋值给 tab 呢?

table 大家都知道,是 HashMap 的一个成员变量,往 map 里面放的数据就存储在这个 table 里面的:

image.png

在 putVal 方法里面,先把 table 赋值给了 tab 这个局部变量,后续在方法里面都是操作的这个局部变量了。

其实,不只是 putVal 方法,在 HashMap 的源码里面,“tab= table” 这样的写发多达 14 个,比如 getNode 里面也是这样的用法:

image.png

我们先思考一下,如果不用 tab 这个局部变量,直接操作 table,会不会有问题?

从代码逻辑和功能上来看,是不会有任何毛病的。

如果是其他人这样写,我会觉得可能是他的编程习惯,没啥深意,反正又不是不能用。

但是这玩意可是 Doug Lea 写的,隐约间觉得必然是有深意在里面的。

image.png


所以为什么要这样写呢?

巧了,我觉得我刚好知道答案是什么。

因为我在其他地方也看到过这种把成员变量赋值给局部变量的写法,而且在注释里面,备注了自己为什么这么写。

而这个地方,就是 Java 的 String 类:

image.png

比如 String 类的 trim 方法,在这个方法里面就把 String 的 value 赋给了 val 这个局部变量。

然后旁边给了一个非常简短的注释:

avoid getfield opcode

本文的故事,就从一行注释开始,一路追溯到 2010 年,我终于抽丝剥茧找到了问题的答案。

一行注释,就是说要避免使用 getfield 字节码。

虽然我不懂是啥意思,但是至少我拿到了几个关键词,算是找到了一个“线头”,接下来的事情就很简单了,顺着这个线头往下缕就完事了。

而且直觉上告诉我这又是一个属于字节码层面的极端的优化,缕到最后一定是一个骚操作。

那么我就先给你说结论了:这个代码确实是 Doug Lea 写的,在当年确实是一种优化手段,但是时代变了,放到现在,确实没有卵用。

网络异常,图片无法展示
|



答案藏在字节码


既然这里提到了字节码的操作,那么接下来的思路就是对比一下这两种不同写法分别的字节码是长啥样的不就清楚了吗?

比如我先来一段这样的测试代码:

public class MainTest {
    private final char[] CHARS = new char[5];
    public void test() {
        System.out.println(CHARS[0]);
        System.out.println(CHARS[1]);
        System.out.println(CHARS[2]);
    }
    public static void main(String[] args) {
        MainTest mainTest = new MainTest();
        mainTest.test();
    }
}

上面代码中的 test 方法,编译成字节码之后,是这样的:

image.png

image.png

在网上随便找个 JVM 字节码指令表,就可以知道这几个字节码分别在干啥事儿:

  • getstatic:获取指定类的静态域, 并将其压入栈顶
  • aload_0:将第一个引用类型本地变量推送至栈顶
  • getfield:获取指定类的实例域, 并将其值压入栈顶
  • iconst_0:将int型0推送至栈顶
  • caload:将char型数组指定索引的值推送至栈顶
  • invokevirtual:调用实例方法

如果,我把测试程序按照前面提到的写法修改一下,并重新生成字节码文件,就是这样的:

image.png

可以看到,getfield 这个字节码只出现了一次。

从三次到一次,这就是注释中写的“avoid getfield opcode”的具体意思。

确实是减少了生成的字节码,理论上这就是一种极端的字节码层面的优化。

具体到 getfield 这个命令来说,它干的事儿就是获取指定对象的成员变量,然后把这个成员变量的值、或者引用放入操作数栈顶。

更具体的说,getfield 这个命令就是在访问我们 MainTest 类中的 CHARS 变量。

往底层一点的说就是如果没有局部变量来承接一下,每次通过 getfield 方法都要访问堆里面的数据。

而让一个局部变量来承接一下,只需要第一次获取一次,之后都把这个堆上的数据,“缓存”到局部变量表里面,也就是搞到栈里面去。之后每次只需要调用 aload_ 字节码,把这个局部变量加载到操作栈上去就完事。

aload_ 的操作,比起 getfield 来说,是一个更加轻量级的操作。

这一点,从 JVM 文档中对于这两个指令的描述的长度也能看出来:

https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.getfield


image.png

就不细说了,看到这里你应该明白:把成员变量赋值到局部变量之后再进行操作,确实是一种优化手段,可以达到“avoid getfield opcode”的目的。

看到这里你的心开始有点蠢蠢欲动了,感觉这个代码很棒啊,我是不是也可以搞一波呢?

不要着急,还有更棒的,我还没给你讲完呢。



stackoverflow


在 Java 里面,我们其实可以看到很多地方都有这样的写法,比如我们前面提到的 HashMap 和 String,你仔细看 J.U.C 包里面的源码,很多都是这样写的。

但是,也有很多代码并没有这样写。

比如在 stackoverflow 就有这样的一个提问:

image.png


提问的哥们说为什么 BigInteger 没有采用 String 的 trim 方法 “avoid getfield opcode” 这样的写法呢?

下面的回答是这样说的:

image.png

目录
相关文章
|
1天前
|
安全 Java 编译器
一个 Bug JDK 居然改了十年?
你敢相信么一个简单的Bug,JDK 居然花了十年时间才修改完成。赶快来看看到底是个什么样的 Bug?
9 1
一个 Bug JDK 居然改了十年?
|
7月前
|
XML 程序员 C#
C注释的高级使用技巧,让你的代码无敌了!
C注释的高级使用技巧,让你的代码无敌了!
35 0
|
7月前
|
Java
注释之背后:代码的解释者与保护者
注释之背后:代码的解释者与保护者
39 0
自 创 日 历 (在代码里有注释讲细节)
做日历主要是要确定好第一天是星期几,然后算间隔多少天,算出具体这一天是星期几,然后把我们想打印的打印出来,把每个月的第一天定位到该在的地方。
78 0
|
Java
【‘’注释‘’】哇哦,这是心动的感觉
【‘’注释‘’】哇哦,这是心动的感觉
83 0
【‘’注释‘’】哇哦,这是心动的感觉
|
Shell
Ansible概述和模块解释(你刚走过了今天,而扑面而来的却是昨天)(二)
Ansible概述和模块解释(你刚走过了今天,而扑面而来的却是昨天)(二)
226 0
Ansible概述和模块解释(你刚走过了今天,而扑面而来的却是昨天)(二)
|
运维 Devops Linux
Ansible概述和模块解释(你刚走过了今天,而扑面而来的却是昨天)(一)
Ansible概述和模块解释(你刚走过了今天,而扑面而来的却是昨天)(一)
207 0
Ansible概述和模块解释(你刚走过了今天,而扑面而来的却是昨天)(一)
|
Shell
Ansible概述和模块解释(你刚走过了今天,而扑面而来的却是昨天)(三)
Ansible概述和模块解释(你刚走过了今天,而扑面而来的却是昨天)(三)
254 0
Ansible概述和模块解释(你刚走过了今天,而扑面而来的却是昨天)(三)
|
程序员
代码界10个最“牛叉”的代码注释
要说到代码注释这个东西吧,其实很神奇,因为不管写不写注释,其实对于代码的运行没有任何的影响,注释的长短也没关系,因为编译器会对于所有的代码注释都是过滤掉的,其实注释非常重要,对后期的代码维护和重构至关重要,但是其实很多程序员童鞋在写代码时往往并不注意注释,所以导致自己回头看自己的代码时也都忘了写的是什么,本文给出了 StackOverflow 网友针对“你看到过的最好的代码注释是什么样的?”这个问题给出的回答的前10条。
6283 0
|
安全 Java 程序员
从源码里的一个注释,我追溯到了12年前,有点意思。 (中)
从源码里的一个注释,我追溯到了12年前,有点意思。 (中)
126 0
从源码里的一个注释,我追溯到了12年前,有点意思。 (中)