配套视频:
为什么推荐大家学习Java字节码
https://www.bilibili.com/video/av77600176/
一、背景
本文主要探讨:为什么要学习 JVM 字节码?
可能很多人会觉得没必要,因为平时开发用不到,而且不学这个也没耽误学习。
但是这里分享一点感悟,即人总是根据自己已经掌握的知识和技能来解决问题的。
这里有个悖论,有时候你觉得有些技术没用恰恰是因为你没有熟练掌握它,遇到可以使用它的场景你根本想不到用。
1.1 从生活的角度来讲
如果你是一个非计算机专业的学生,你老师给你几张图书的拍照,大概3000字,让你打印成文字。
你打开电脑,噼里啪啦一顿敲,搞了一下午干完了。
如果你知道语音输入,那么你可能采用语音输入的方式,30分钟搞定。
如果你了解 OCR 图片文字识别,可能 5 分钟搞定。
不同的方法,带来的效果完全不同。然而最可怕的是,你不会语音输入或者OCR你不会觉得自己少了啥。
OCR识别绝对不是你提高点打字速度可以追赶上的。
1.2 学习Java的角度
很多人学习知识主要依赖百度,依赖博客,依赖视频和图书,而且这些资料质量参差不齐,而且都是别人理解之后的结果。
比如你平时不怎么看源码,那么你就很少能将源码作为你学习的素材,只能依赖博客、图书、视频等。
如果你平时喜欢看源码,你会对源码有自己的理解,你会发现源码对你的学习有很多帮助。
如果你平时不怎么用反编译和反汇编,那么你更多地只能依赖源码,依赖调试等学习知识,而不能从字节码层面来学习和理解知识。
当你慢慢熟练读懂虚拟机指令,你会发现你多了一个学习知识的途径。
二、为什么要学习字节码
2.1 人总是不愿意离开舒适区的
很多人在学习新知识时,总是本能地抵触。会找各种理由不去学,“比如暂时用不到”,“学了没啥用”,“以后再说”。
甚至认为这是在浪费时间。
2.2 为什么要学习字节码?
最近学习了一段时间 JVM 字节码的知识,虽然不算精通,但是读字节码起来已经不太吃力。
为什么推荐学习字节码是因为它可以从比源码更深的层面去学习 Java 相关知识。
虽然不可能所有问题都用字节码的知识来解决,但是它给你一个学习的途径。
比如通过字节码的学习你可以更好地理解 Java中各种语法和语法糖背后的原理,更好地理解多态等语言特性。
三、举例
本文举一个简单的例子,来说明学习字节码的作用。
3.1 例子
3.1.1 语法糖
-
public
class ForEachDemo {
-
-
public static void main(String[] args) {
-
-
List<String> data =
new ArrayList<>();
-
data.add(
"a");
-
data.add(
"b");
-
for (String str : data) {
-
System.out.println(str);
-
}
-
}
-
}
编译: javac ForEachDemo.java
反汇编:javap -c ForEachDemo
-
public
class com.imooc.basic.learn_source_code.local.ForEachDemo {
-
public com.imooc.basic.learn_source_code.local.ForEachDemo();
-
Code:
-
0: aload_0
-
1: invokespecial #
1
// Method java/lang/Object."<init>":()V
-
4:
return
-
-
public static void main(java.lang.String[]);
-
Code:
-
0:
new #
2
// class java/util/ArrayList
-
3: dup
-
4: invokespecial #
3
// Method java/util/ArrayList."<init>":()V
-
7: astore_1
-
8: aload_1
-
9: ldc #
4
// String a
-
11: invokeinterface #
5,
2
// InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
-
16: pop
-
17: aload_1
-
18: ldc #
6
// String b
-
20: invokeinterface #
5,
2
// InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
-
25: pop
-
26: aload_1
-
27: invokeinterface #
7,
1
// InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
-
32: astore_2
-
33: aload_2
-
34: invokeinterface #
8,
1
// InterfaceMethod java/util/Iterator.hasNext:()Z
-
39: ifeq
62
-
42: aload_2
-
43: invokeinterface #
9,
1
// InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
-
48: checkcast #
10
// class java/lang/String
-
51: astore_3
-
52: getstatic #
11
// Field java/lang/System.out:Ljava/io/PrintStream;
-
55: aload_3
-
56: invokevirtual #
12
// Method java/io/PrintStream.println:(Ljava/lang/String;)V
-
59: goto
33
-
62:
return
-
}
我们可以清晰地看到foreach 循环底层用到了迭代器实现,甚至可以逆向脑补出对应的Java源码(大家可以尝试根据字节码写出等价的源码)。
3.1.2 读源码遇到的一个问题
我们在读源码时经常会遇到类似下面的这种写法:
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#startWebServer
-
private WebServer startWebServer() {
-
WebServer webServer =
this.webServer;
-
if (webServer !=
null) {
-
webServer.start();
-
}
-
return webServer;
-
}
在函数中声明一个和成员变量同名的局部变量,然后将成员变量赋值给局部变量,再去使用。
看似很小的细节,隐含着一个优化思想。
可能有些人读过某些文章有提到(可是为什么我们总得看到一个文章会一个知识?如果没看到怎么办?),更多的人可能并不能理解有什么优化。
3.2 模拟
普通的语法糖这里就不做过多展开,重点讲讲第二个优化的例子。
模仿上述写法的例子:
-
public
class LocalDemo {
-
-
private List<String> data =
new ArrayList<>();
-
-
public void someMethod(String param) {
-
List<String> data =
this.data;
-
if (data !=
null && data.size() >
0 && data.contains(param)) {
-
System.out.println(data.indexOf(param));
-
}
-
-
}
-
-
}
编译:javac LocalDemo.java
反汇编: javap -c LocalDemo
-
public
class com.imooc.basic.learn_source_code.local.LocalDemo {
-
public com.imooc.basic.learn_source_code.local.LocalDemo();
-
Code:
-
0: aload_0
-
1: invokespecial #
1
// Method java/lang/Object."<init>":()V
-
4: aload_0
-
5:
new #
2
// class java/util/ArrayList
-
8: dup
-
9: invokespecial #
3
// Method java/util/ArrayList."<init>":()V
-
12: putfield #
4
// Field data:Ljava/util/List;
-
15:
return
-
-
public void someMethod(java.lang.String);
-
Code:
-
0: aload_0
-
1: getfield #
4
// Field data:Ljava/util/List;
-
4: astore_2
-
5: aload_2
-
6: ifnull
41
-
9: aload_2
-
10: invokeinterface #
5,
1
// InterfaceMethod java/util/List.size:()I
-
15: ifle
41
-
18: aload_2
-
19: aload_1
-
20: invokeinterface #
6,
2
// InterfaceMethod java/util/List.contains:(Ljava/lang/Object;)Z
-
25: ifeq
41
-
28: getstatic #
7
// Field java/lang/System.out:Ljava/io/PrintStream;
-
31: aload_2
-
32: aload_1
-
33: invokeinterface #
8,
2
// InterfaceMethod java/util/List.indexOf:(Ljava/lang/Object;)I
-
38: invokevirtual #
9
// Method java/io/PrintStream.println:(I)V
-
41:
return
-
}
此时 局部变量表中 0 为 this , 1 为 param 2 为 局部变量 data
直接使用成员变量的例子:
-
-
public
class ThisDemo {
-
-
-
private List<String> data =
new ArrayList<>();
-
-
public void someMethod(String param) {
-
-
if (data !=
null && data.size() >
0 && data.contains(param)) {
-
System.out.println(data.indexOf(param));
-
}
-
-
}
-
}
编译:javac ThisDemo.java
反汇编: javap -c ThisDemo
-
public
class com.imooc.basic.learn_source_code.local.ThisDemo {
-
public com.imooc.basic.learn_source_code.local.ThisDemo();
-
Code:
-
0: aload_0
-
1: invokespecial #
1
// Method java/lang/Object."<init>":()V
-
4: aload_0
-
5:
new #
2
// class java/util/ArrayList
-
8: dup
-
9: invokespecial #
3
// Method java/util/ArrayList."<init>":()V
-
12: putfield #
4
// Field data:Ljava/util/List;
-
15:
return
-
-
public void someMethod(java.lang.String);
-
Code:
-
0: aload_0
-
1: getfield #
4
// Field data:Ljava/util/List;
-
4: ifnull
48
-
7: aload_0
-
8: getfield #
4
// Field data:Ljava/util/List;
-
11: invokeinterface #
5,
1
// InterfaceMethod java/util/List.size:()I
-
16: ifle
48
-
19: aload_0
-
20: getfield #
4
// Field data:Ljava/util/List;
-
23: aload_1
-
24: invokeinterface #
6,
2
// InterfaceMethod java/util/List.contains:(Ljava/lang/Object;)Z
-
29: ifeq
48
-
32: getstatic #
7
// Field java/lang/System.out:Ljava/io/PrintStream;
-
35: aload_0
-
36: getfield #
4
// Field data:Ljava/util/List;
-
39: aload_1
-
40: invokeinterface #
8,
2
// InterfaceMethod java/util/List.indexOf:(Ljava/lang/Object;)I
-
45: invokevirtual #
9
// Method java/io/PrintStream.println:(I)V
-
48:
return
-
}
此时局部变量表只有两个,即 this 和 param。
大家也可以通过 javap -c -v 来查看更详细信息,本例截图中用到 IDEA 插件为jclasslib bytecode viewer,感兴趣参考我的另外一篇对该工具的介绍博文:《IDEA字节码学习查看神器jclasslib bytecode viewer介绍》。
3.3 分析
通过源码其实我们并不能很好的理解到底优化了哪里。
我们分别对两个类进行编译和反汇编后可以清晰地看到:第一个例子代码多了一行,反而反编译后的字节码更短。
第二个例子反编译后的字节码比第一个例子长在哪里呢?
我们发现主要多在:getfield #4 // Field data:Ljava/util/List; 这里。
即每次获取 data对象都要先 aload_0 然后再 getfield 指令获取。
第一个例子通过 astore_2 将其存到了局部变量表中,每次用直接 aload_2 直接从局部变量表中加载到操作数栈。
从而不需要每次都从 this 对象中获取这个属性,因此效率更高。
这种思想有点像写代码中常用的缓存,即将最近要使用的数据先查一次缓存起来,使用时优先查缓存。
本质上体现了操作系统中的时间局部性和空间局部性的概念(不懂的话翻下书或百度下)。
因此通过字节码的分析,通过联系实际的开发经验,通过联系专业知识,这个问题我们就搞明白了。
另外也体现了用空间换时间的思想。
知识只有能贯穿起来,理解的才能更牢固。
此处也体现出专业基础的重要性。
另外知识能联系起来、思考到本质,理解才能更深刻,记忆才能更牢固,才更有可能灵活运用。
四、总结
这只是其中一个非常典型的例子,学习 JVM 字节码能够给你一个不一样的视角,让你多一个学习的途径。
可能很多人说自己想学但是无从下手,这里推荐大家先看《深入理解Java虚拟机》,然后结合《Java虚拟机规范》,平时多敲一下 javap 指令,慢慢就熟悉了,另外强力推荐jclasslib bytecode viewer插件,该插件可以点击指令跳转到 Java虚拟机规范对该指令的介绍的部分,对学习帮助极大。
很多人可能会说,学这个太慢。
的确,急于求成怎么能学的特别好呢?厚积才能薄发,耐不住寂寞怎么能学有所成呢。
本文通过这其中一个例子让大家理解,JVM字节码可以帮助大家理解Java的一些语法(篇幅有限,而且例子太多,这里就不给出了,感兴趣的同学自己尝试),甚至帮助大家学习源码。
试想一下,如果你认为学习字节码无用,甚至你都不了解,你怎么可能用它来解决问题呢?
你所掌握的知识帮助你成长由限制了你的成长,要敢于突破舒适区,给自己更多的成长机会。
-------------------
欢迎点赞、评论、转发,你的鼓励,是我创作的动力。
转载:https://blog.csdn.net/w605283073/article/details/103247217