从字节码角度分析一波 Spring 源码中隐藏知识点!

共 17837字,需浏览 36分钟

 ·

2021-08-27 19:08

你知道的越多,不知道的就越多,业余的像一棵小草!

你来,我们一起精进!你不来,我和你的竞争对手一起精进!

编辑:业余草

blog.csdn.net/w605283073

推荐:https://www.xttblog.com/?p=5263

一、背景

这周一我面试了一个 Java 程序员,工作了 6 年了。我问的第一个问题就是“Redis 能用来做什么?你们系统中使用了 Redis 的哪些功能?”,他回答说是缓存

缓存确实是 Redis 使用最多的领域。可当我再进一步问“还有呢?”,他想了一会说:“分布式锁”。然后我就分布式锁再深入问下去,他开始摇头:我们项目里面 Redis 的锁方法都是别人(应该是架构师)封装好的,拿过来直接使用,内部细节没有去了解过,也没有必要了解

然后我和他说了,Redis 有很多使用场景,参考《面试官:说出16个Redis常见的使用场景!》。

然后我和他讲了庖丁解牛和蛮夫剁骨头的故事,他认为船到桥头自然直,他没有用和学 Redis 的这些功能是因为项目中没用使用的场景和必要。

可能很多人会觉得没必要,因为平时开发用不到,而且不学这个也没耽误学习

但是这里分享一点感悟,即人总是根据自己已经掌握的知识和技能来解决问题的

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

从生活的角度来讲

如果你是一个非计算机专业的学生,你老师给你几张图书的拍照,大概 3000 字,让你打印成文字。

你打开电脑,噼里啪啦一顿敲,搞了一下午干完了。

如果你知道语音输入,那么你可能采用语音输入的方式,30 分钟搞定。

如果你了解 OCR 图片文字识别,可能 5 分钟搞定。

不同的方法,带来的效果完全不同。然而最可怕的是,你不会语音输入或者 OCR,你不会觉得自己少了啥。

OCR 识别绝对不是你提高点打字速度可以追赶上的。

学习 Java 的角度

很多人学习知识主要依赖百度,依赖博客,依赖视频和图书,而且这些资料质量参差不齐,而且都是别人理解之后的结果。

比如你平时不怎么看源码,那么你就很少能将源码作为你学习的素材,只能依赖博客、图书、视频等。

如果你平时喜欢看源码,你会对源码有自己的理解,你会发现源码对你的学习有很多帮助。

如果你平时不怎么用反编译和反汇编,那么你更多地只能依赖源码,依赖调试等学习知识,而不能从字节码层面来学习和理解知识。

当你慢慢熟练读懂虚拟机指令,你会发现你多了一个学习知识的途径。

昨天的文章

昨天我发的那篇文章(Java For循环的十一种优化方案!),里面也有很多知识点,但是很多人看不上,因为单个优化的效果并不明显。但是你要知道 HikariCP 之所以成为最快的数据库,就是因为作者的重点细小优化做到的。

LongAdder 之所以性能牛叉(面试官:为什么LongAdder性能比long还快),也是因为作者不但的研究和突破,分而治之的通过细微的调整,将 AtomicLong 远远甩在身后,而作者的论文也在业界享有盛誉!

人总是不愿意离开舒适区的

很多人在学习新知识时,总是本能地抵触。会找各种理由不去学,“比如暂时用不到”,“学了没啥用”,“以后再说”。

甚至认为这是在浪费时间。

为什么要学习字节码?

这几年我 Watch 了不少知名开源框架,通过它们,我学习了一些 JVM 字节码的知识(深入理解JVM方法调用的内部机制),虽然不算精通,但是读字节码起来已经不太吃力。

因此,我认为学会字节码可以从比源码更深的层面去学习 Java 相关知识。

虽然不可能所有问题都用字节码的知识来解决,但是它给你一个学习的途径。

比如通过字节码的学习你可以更好地理解 Java中各种语法和语法糖背后的原理,更好地理解多态等语言特性。

语法糖

下面通过举一个简单的例子,来说明学习字节码的作用。

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
       4return

  public static void main(java.lang.String[]);
    Code:
       0new           #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
      62return
}

我们可以清晰地看到 foreach 循环底层用到了迭代器实现,甚至可以逆向脑补出对应的 Java 源码(大家可以尝试根据字节码写出等价的源码)。

读源码遇到的一个问题

我们在读源码时经常会遇到类似下面的这种写法。

org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#startWebServer中的源码:

 private WebServer startWebServer() {
  WebServer webServer = this.webServer;
  if (webServer != null) {
   webServer.start();
  }
  return webServer;
 }

在函数中声明一个和成员变量同名的局部变量,然后将成员变量赋值给局部变量,再去使用。

看似很小的细节,隐含着一个优化思想

可能有些人读过某些文章有提到(为什么我们总得看到一个文章学会一个知识?如果没看到怎么办?),更多的人可能并不能理解有什么优化。

模拟

普通的语法糖这里就不做过多展开,重点讲讲第二个优化的例子。

模仿上述写法的例子:

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
       5new           #2                  // class java/util/ArrayList
       8: dup
       9: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
      12: putfield      #4                  // Field data:Ljava/util/List;
      15return

  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
      41return
}

此时局部变量表中 0 为 this,1 为 param  2 为 局部变量 data。

Java局部变量表

直接使用成员变量的例子:


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
       5new           #2                  // class java/util/ArrayList
       8: dup
       9: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
      12: putfield      #4                  // Field data:Ljava/util/List;
      15return

  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
      48return
}

此时局部变量表只有两个,即 this 和 param。

局部变量表

大家也可以通过 javap -c -v 来查看更详细信息,本例截图中用到 IDEA 插件为jclasslib bytecode viewer

分析

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

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

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

我们发现主要多在: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的一些语法(篇幅有限,而且例子太多,这里就不给出了,感兴趣的同学自己尝试),甚至帮助大家学习源码。

试想一下,如果你认为学习字节码无用,甚至你都不了解,你怎么可能用它来解决问题呢?

「你所掌握的知识帮助你成长由限制了你的成长,要敢于突破舒适区,给自己更多的成长机会。」

欢迎点赞、评论、转发,你的鼓励,是我创作的动力。

浏览 46
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报