现在的位置: 首页 > 综合 > 正文

为什么要学习字节码

2020年02月24日 综合 ⁄ 共 5977字 ⁄ 字号 评论关闭

  一、背景

  可能很多人会觉得没必要,因为平时开发用不到,而且不学这个也没耽误学习。但是这里分享一点感悟,即人总是根据自己已经掌握的知识和技能来解决问题的。

  这里有个悖论,有时候你觉得有些技术没用恰恰是因为你没有熟练掌握它,遇到可以使用它的场景你根本想不到用。

  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 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."":()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."":()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 data = new ArrayList<>();

  public void someMethod(String param) {

  List 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."":()V

  4: aload_0

  5: new #2 // class java/util/ArrayList

  8: dup

  9: invokespecial #3 // Method java/util/ArrayList."":()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 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."":()V

  4: aload_0

  5: new #2 // class java/util/ArrayList

  8: dup

  9: invokespecial #3 // Method java/util/ArrayList."":()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。

  3.3 分析

  通过源码其实我们并不能很好的理解到底优化了哪里。

  我们分别对两个类进行编译和反汇编后可以清晰地看到:第一个例子代码多了一行,反而反编译后的字节码更短。

  第二个例子反编译后的字节码比第一个例子长在哪里呢?

  我们发现主要多在:getfield #4 // Field data:Ljava/util/List; 这里。

  即每次获取 data对象都要先 aload_0 然后再 getfield 指令获取。

  第一个例子通过 astore_2 将其存到了局部变量表中,每次用直接 aload_2 直接从局部变量表中加载到操作数栈。

  从而不需要每次都从 this 对象中获取这个属性,因此效率更高。

  这种思想有点像写代码中常用的缓存,即将最近要使用的数据先查一次缓存起来,使用时优先查缓存。

  本质上体现了操作系统中的时间局部性和空间局部性的概念(不懂的话翻下书或百度下)。

  因此通过字节码的分析,通过联系实际的开发经验,为什么要学习字节码通过联系专业知识,这个问题我们就搞明白了。

  另外也体现了用空间换时间的思想。知识只有能贯穿起来,理解的才能更牢固。此处也体现出专业基础的重要性。

  另外知识能联系起来、思考到本质,理解才能更深刻,记忆才能更牢固,才更有可能灵活运用。

  四、总结

  这只是其中一个非常典型的例子,学习 JVM 字节码能够给你一个不一样的视角,让你多一个学习的途径。

抱歉!评论已关闭.