深入理解JVM 读懂java字节码

作者: 撸代码的大白 | 来源:发表于2020-03-28 12:07 被阅读0次

    一、从源代码到本地代码。

    我们都知道,机器是读不懂我们的高级语言的,从高级语言到机器语言,需要经历两个比较大的阶段(前端编译和后端编译),虽然我们今天的主题是前端编译,不过我觉得有必要系统的阐述一下。下面盗个图解释一下,毕竟画图太耗时间。这个是编译原理:

    从源代码到机器码

    用java的话简单说,前端编译负责由.java编译成.class(javac);后端编译负责从.class生成机器码(解释器、JIT)。

    前端编译:词法分析(将字符序列转换为标记序列)、语法分析(构造抽象语法树,用于描述程序语法结构是否正确)、语义分析(对结构上正确的源程序进行上下文有关性质的审查,进行类型审查)

    通俗的来讲,可以理解为,你写了一篇作文,老师先把你的作文一个字一个字的读进去,然后分成字、词语、标点等等,这个是词法分析。然后呢,看看你分了几个段,段头又没有空格、符不符合主谓宾,有没有标题,有没有句号,括号是不是只有一边啥的,这是语法分析。最后通篇检查你写的符不符合逻辑,有没有前言不搭后语啥的,这是语义分析。

    当然,这个类比并不严格,大体理解意思,知道每个阶段的关键作用就行了。

    后端编译:主要分解释编译和即时编译。解释编译可以简单理解为一条一条的将字节码翻译成机器码。即时编译简单理解就是把一大段字节码翻译成机器码。这两个通俗的说法区别于有没有卡顿。解释执行一条一条没有卡顿但是执行效率低、即时编译一下一大段有卡顿,但是执行效率高。一般是结合使用的,一般逻辑解释执行、热点代码即时编译。有些对卡顿现象不敏感的场景,可以全部使用JIT。还有一种是AOT,就是直接从源代码到机器码,明显优点是保护代码,难反编译,特定场景下性能很好,缺点是对运行时支持不好,尤其是动态代理等场景。

    再盗个图,毕竟画图能力捉鸡:


    从.java到native

    二、反编译字节码

    字节码长啥样,我们先来一段简单的源代码:

    一个简单的类

    我们来看看这段源代码产生的字节码长啥样:

    student字节码

    长这样婶的。是不是有点懵,脑瓜子嗡嗡的。别急,咱们一点点的把这个字节码给反编译一下。通过这个过程,理解一下字节码的结构。

    先科普一下基本知识,上面的图是16进制的,我们知道一个字节是8位,也就是2^8,2^4*2所以两个16进制表示一个字节。

    要翻译,得找本字典,字典来了:

    字典一:字节码结构图

    class文件结构

    从这个结构上,我们发现,4个字节的魔数、2个字节次版本、2个字节主版本、2个字节常量池常量个数、然后跟着一个不定长的常量数组、2个字节访问标志、2个字节类本身信息、2个字节父类信息、2个字节接口数量、2个字节接口名数组、2个字节变量个数、然后跟着变量数组、2个字节方法个数、然后跟着方法数组、2个字节附加属性数量、然后是附加属性数组

    4个字节魔数:

    cafe babe

    每个类都是以这个魔数开头的,这是高司令定的,对于这个魔数,我是这么理解的,JAVA是一种咖啡,每个class都是这个cafe的babe,那为啥不是baby呢,因为16进制没有y。

    2个字节主版本、2个字节次版本:


    版本信息

    0000代表次版本是0 ;0034是52,52代表1.8;所以版本是1.8.0

    2个字节是常量量个数:


    常量个数

    0022说明有34个常量,我们看看反编译工具的结果:

    常量池

    我们发现一共33个,不是34个,那0号呢?0号预留了,为异常处理留的。

    后面紧跟着常量表的内容。我们先上一下常量池的字典再说

    常量池元素类型表

    有人会问了,常量池是啥,干嘛的。笼统点的解释就是,后面的各结构里能用到的资源,包括一些能反复使用的字符串、还有一些类型信息啥的。咱们都写过脚本吧,有些执行脚本里,遇到一些常量啥的,我们都喜欢抽出来,然后脚本里面可以反复的引用。

    这个常量池就好像一个资源预先准备的地方,之后用到的东西,很多都直接给各常量号就行了,不用再写一遍了。这样可以最大的节省空间。

    我们一个常量一个常量的解析。

    第1个常量:

    首先1个字节是常量类型:

    第一个常量的类型

    0a表示是10,查字典发现是

    符号引用

    之前咱们讲过,类的初始化在连接阶段的解析阶段会将符号引用转变为直接引用,当时没有解释啥是符号引用、这个就是符号引用。因为编译阶段不知道具体的对象在哪儿,只能用类型加全名来表示引用,称为符号引用。

    言归正传,后面有2个字节是类描述符的索引和2个字节名称和类型的索引

    指向第5个常量和第29个常量。咱们还没解释出来这俩,先用工具看看是啥

    第5个是这样婶的:

    可以看到他又指向了第33个

    第33是这样的,是个utf8的字符串。为啥第一个不直接指向33呢,因为类型不同。

    第29的是这样的:

    又指向了10 和 11

    这俩组合起来就是"<init>":()V 最后组合起来就是:

    表示java/lang/Object.的传参为空返回为空的构造方法。从第一个常量,我们可以看出规律来,最基本的是Utf8类型,相当于是基本的材料,组合这些材料,产出一个相对复杂的常量。产生的常量又可以作为之后程序中使用的材料。

    第2个常量:


    这是个类的成员变量的描述信息:

    指向第4个常量和第30个常量:

    表示student类里的int型名字为age的变量:

    第3个常量:


    指向第4个和第31个常量

    结果为,表示student类类型为String名称为name的变量:

    第4个常量:

    指向第32个常量:

    有字典对着可以一直翻译完。翻译完就是咱们执行 javap -verbose classpath/com/dabai/test/student.class的结果是一致的。这里提供各个阶段的字典。

    Access_Flag对应表:

    field结构:

    method结构:

    code:

    其中method反编译比较麻烦,我们以setName为例子,讲解一下。

    首先2个字节的access_flag:0001:ACC_PUBLIC   0013指向第19个常量:

    0014指向第20个常量:

    就是说,方法是public的、传入一个String类型的参数、返回时空的。

    然后又2个字节的属性个数0002,有两个。先看第一个。2位属性名称索引:000C:

    名称为 code 。4位属性长度:

    属性长度:62个字节

    2位最大栈深度:0002,2位内部变量:0002个,4位长度:00000006

    代码:2a 对应代码:aload_0:加载第0个内部变量,

    2b 对应代码:aload_1:加载1个内部变量

    b5 对应代码:putfield

    0003 对应常量池第3个常量即name:

    b1:return

    之后两个字节跟着是异常表长度:0000

    然后是2个字节code属性个数:0002

    第一个code属性:2个字节的属性名称:000d 对应第13个常量:

    这个表示行号表,行号表结构如下:

    后面跟着4字节的长度信息:0000000a,表示有长度为10

    0002:代表两个行号对应

    0000:字节码code的第0行,对应原代码的第000c 12行

    0005:字节码code的第5行,对应原代码的第000d 13行

    后面跟着的是本地变量表,就是内部变量000e:

    4个字节表示长度:00000016:22

    0002:代表变量个数,这里我们发现,命名只有一个变量,也就是传入的name,为什么是两个呢

    我们继续反编译,0000:开始位置 0006:长度6     000f:第15个常量

    后面是描述:0010:第16个常量,表示对象本身

    0000:slot:0

    然后是第二个变量:0000:开始为止 0006:长度 0006:第6个常量:

    然后是描述:0007:第7个常量:

    0001:slot:1

    后面是方法的第二个属性,跟code属性平级。方法传入的参数。

    0015:第21个常量,表示方法参数

    00000005:长度为5

    01:第一个参数,这里还可以回答我们一个问题,java的方法最多可以接受多少个参数,答案是ff即255个

    0006:第6个常量:

    0000:AcessFlag

    最后,展示一下反编译之后的常量池和方法setName

    方法setname:

    这篇终于写完了,真是费劲。有些人可能觉得这么做一遍没啥意义,各花入各眼,仁者见仁吧。

    相关文章

      网友评论

        本文标题:深入理解JVM 读懂java字节码

        本文链接:https://www.haomeiwen.com/subject/iaokuhtx.html