JVM系列:几张图看懂Java字节码

作为一个java程序员,如果你不懂字节码的话,你只能算是初级程序员了。,这可不是耸人听闻。了解字节码你才能真正了解包括“动态代理的原理”、“类加载的细节过程”、“重载和重写是如何实现的”、“多态是如何实现的”、“泛型究竟是什么”等等。,了解这些对你的工作有实际的意义:,比如,开发时碰到问题,需要确定一个jar包是不是最新的(开发时我们经常使用一个SNAPSHOT版本反复打包)。,比如,你在工具组,要开发一个动态修改运行中的类的工具。,比如,你碰到大厂高阶一些的面试,尤其是偏底层的团队。,比如,你碰到各种诡异的包括类找不到、方法找不到、java版本错误无法解析类等问题。,了解字节码就仿佛给了你另一双眼睛和更高纬度的视角,来应对各种java相关的工作。,话不多说,我们这就开始。,图片图片,如图所示,我们写好的java代码会先编译成字节码,然后JVM会加载这些字节码并执行其中的命令,也就是将字节码翻译为机器码执行。,这里有两个重点:,1)其他语言写的代码,如果可以翻译成标准的java字节码,一样可以在JVM中运行、事实上,有一些语言已经可以这么做了(包括Groovy、JRuby、Scala、Clojure、Kotlin等)。这是字节码技术面向未来的最大卖点,也是JVM的宏大愿景。,2)如果我们改变了编译出来字节码,其实就改变了逻辑。不需要去改原来的java代码。,下面我们用一个例子来看看字节码到底是什么,长啥样。,这段逻辑很简单。我定义了一个Calculator类,它有一个add方法,就是传入两个数值参数,把他们相加后再减去一个固定值offset,然后返回。,我们使用javac命令编译后,得到如下文件(十六进制):,图片图片,这个class人肉读起来非常费劲。他有其固定的格式,你需要按照“说明书”一个字节一个字节翻译过来才行。,这里我不做详细的介绍,看了下图你基本上就能大致了解了。,图片图片,一般我们看编译后的文件,绝对不会直接看十六进制的。而是使用javap命令,将其转化为我们更容易看懂的格式。我们来执行下命令:,得到如下这样的内容:,图片图片,不要着急,我来逐块分开和你讲讲。,我们将字节码的内容分为三块来讲:类信息、常量池和方法表。,图片,这里面的内容不多,也比较容易理解。,major version: 52说明这是一个使用java8编译出来的class。,flags:ACC_PUBLIC表示这个class是public的。ACC_SUPER在JDK1.2以后编译出来的类都会带上这个标志,这和JDK1.2以后invokespecial指令的变化有关。,常量池中保存着非常丰富的信息。包括:方法的符号引用、类的符号引用、字段或方法描述符、各种字符常量、各种基础类型字面量等等。你可以简单理解为各种你定义的类名称、属性名称、方法名称、常量以及系统自身需要的一些字面量,都会在这里定义。,下图是Calculator类的常量池数据,我做了一些标注,方便你的理解。,图片图片,常量池的第一列是“常量类型”,一共有十几种。每一种决定了第二列的数据格式。我这里就不详细贴图了。,常量池的第二列会有其他一些常量信息的引用,这些引用往往还是嵌套的。不过如图所示,每个复杂的常量后都有注解帮你拼接好了。,在Calculator类中,有两个方法。其中一个是我们定义的add方法,另一个则是默认构造函数。,【方法1 – 构造函数】,我们先来看下默认构造函数。和上面一样,我直接在图中做了说明,方便理解:,图片图片,【方法2 – add函数】,在介绍add方法前,我们要先简单介绍下JVM的执行引擎。,JVM的执行引擎称之为“基于栈的执行引擎”。也就是说,所有计算的中间结果都存在一个专门的栈中。与之相对应的就是历史更悠久的经典“基于寄存器的操作引擎”。,我们用一个 x + y * z 的例子,来看下两种操作引擎的差别。见下图,图片图片,可以看到,完全一样的代码,寄存器的每个指令都需要多个参数,而基于栈的操作指令只需要一条。这是因为寄存器需要指定数据从哪个寄存器中拿(cpu有十多个寄存器),但基于栈的操作指令只会涉及一个操作数栈,所以只需要声明出栈或者入栈即可。,下面就是 x + y * z 这条命令对应的字节码操作过程:,图片图片,介绍完这个后,我们就可以看下Calculator类中add方法的字节码内容了:,图片图片,到这里,我们就把字节码中主要的三块内容都讲完了。,既然我们了解了字节码的结构,我们就要实践一下直接定位并修改字节码,不然的话只是纸上功夫了。,我们写了个main函数来调用上面的Calculator类,这里把Main和Calculator类都贴一下:,我们编译并运行一下,得到如下结果:,结果为 1 + 2 – 100 = -97,下面我来把add方法中的第7行通过字节码改成 int m = z + offset。,图片图片,修改后的class我们通过javap反编译后能看到,命令已经从isub变成了iadd。我们来运行下看看结果:,这次变成了 1 + 2 + 100 = 103。可以看到,我们并没有重新编译,所以你只要找对地方修改正确,就可以直接改变代码逻辑了。,当然,我并不鼓励你去修改线上的class。你也看到了,十六进制的代码是非常容易改错的。上面这个例子更多的是帮你理解java的字节码。,看了上面的内容,我不知道你是否联想到了Spring中的AOP。事实上Cglib就是通过直接修改字节码的方式来实现切面的。这点我相信你准备面试的时候肯定已经知道了,但是通过上面的内容,我相信你肯定有更深的理解了。,但事实上,更重要的话题是,要如何方便地修改字节码(不直接修改十六进制文件),这是我们下面一篇JVM系列文章会给大家介绍的内容。,此外,这些都只是编译过程中“耍的小花招”,如何重载一个运行时的类是更有意思的话题。虽然现在很多热部署方案存在各种各样的问题,从而大家都还是信赖静态编译后重新部署,但是了解对运行时类的修改和重载,是极有意义的,尤其是开发过程中。,本文转载自微信公众号「 CodingBetterLife」,作者「 赵志强 」,可以通过以下二维码关注。,JVM系列:几张图看懂Java字节码,转载本文请联系「 CodingBetterLife」公众号。

文章版权声明

 1 原创文章作者:cmcc,如若转载,请注明出处: https://www.52hwl.com/28204.html

 2 温馨提示:软件侵权请联系469472785#qq.com(三天内删除相关链接)资源失效请留言反馈

 3 下载提示:如遇蓝奏云无法访问,请修改lanzous(把s修改成x)

 免责声明:本站为个人博客,所有软件信息均来自网络 修改版软件,加群广告提示为修改者自留,非本站信息,注意鉴别

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023年6月23日
下一篇 2023年7月15日