美文网首页java 虚拟机(JVM)学习笔记
从字节码角度仔细剖析一个HelloWorld程序

从字节码角度仔细剖析一个HelloWorld程序

作者: 半岛铁盒里的猫 | 来源:发表于2019-10-13 16:44 被阅读0次

    本文是我发布在csdn的一篇技术博文:https://blog.csdn.net/sinat_23092639/article/details/100170726

    前言:

    由于本文所讨论的内容比较枯燥,而且很长,有大量图片,所以建议在安静的地方用电脑花一段时间耐心阅读,笔者也尽量用轻松的方式讲解。

    **1. 变身的HelloWorld

    1. 初识字节码
    2. 解析HelloWorld字节码
    3. 结束语**

    变身的HelloWorld

    相信学过Java的朋友都知道HelloWorld是什么,当年第一次成功运行HelloWorld程序的激动似乎还宛如昨天的事,历历在目。没错,今天就讲一个HelloWorld的Java程序~

    public class HelloWorld {
    
        public static void main(String[] args) {
            int a = 1 + 1;
        }
    }
    

    还有比这个更简单的Java程序么?答案是没有。那还讲啥?列位看官别急,今天讲的HelloWorld 是这样子的:
    图1:


    HelloWord

    估计许多Java工程师看了都是一脸懵逼(懵逼也正常,谁看到一堆十六进制数不懵逼呢?)

    当然既然大家已经看过文章标题,想必就知道这是什么了?没错,它就是HelloWorld 的字节码。

    刚才图1是怎么得来的呢?使用javac编译HelloWorld 的Java源文件,将得到的HelloWorld.class文件,将用Binary viewer等字节码查看工具打开即可。

    初识字节码

    以下概念说明摘抄于《深入理解Java虚拟机》

    字节码就是Java经过javac编译生成的数据格式,也就是常说的Class文件。我们都知道Java的一大特点是平台无关特性,也就是不同平台只需要一份Java代码就足够。那构成平台无关特性的基石就是字节码,因为不同平台的虚拟机都是用统一的程序格式,也就是字节码。Java还有另一大特点就是语言无关性,虚拟机不和包括Java在内的语言绑定,也就是说虚拟机只认字节码,从不关心字节码来自什么语言。所以通过平台以及语言无关性,我们不管写什么语言,只要是基于Java虚拟机的,就可以运行在不同的平台上。
    字节码文件包含了Java虚拟机指令集和符号表以及若干辅助信息。它是一组以8个字节为基础单位的二进制信息流,各项数据项目按照严格顺序紧凑排列在Class文件中,中间没有任何分隔符 Class文件采用类似C语言结构体的伪结构来存储数据,这种伪结构只有两种数据:无符号数和表(表就是无符号数和其他的表组成(无符号数和表有点类型基本数据类型和对象的味道))。

    解析HelloWorld字节码

    好了,看了一段晦涩的概念,还是进入我们的HelloWorld吧~

    首先看一张很重要的表,接下来的分析每一步都要基于这张表,这张表就是我们的Class的一个整体结构图,也可以说是整个Class文件的总地图:

    图2:


    字节码文件整体结构表

    (温馨提示:后面会经常提到该表格,为了之后的阅读体验,建议先将此表格保存在本地)

    整个Class文件就是按照这个表的顺序储存的,其中左边的类型的ux表示无符号数,比如u2表示两个字节,xx_info就是数据类型的表。

    看下最开始的部分:
    图3:


    在这里插入图片描述

    意思就是开头的4个字节是魔数(不是魔术),它的作用很简单,就是确定这个文件是否是一个能被虚拟机接受的Class文件,使用魔数而不是使用扩展名来进行识别主要是出于安全方面的考虑。看下我们的HelloWorld:
    图4:


    在这里插入图片描述
    看,我们的Java设计者似乎很有浪漫气息,而且对咖啡情有独钟(因为这是一个著名的咖啡品牌)。是的,目前所有的Class文件魔数都是CAFEBABE,也就是说,如果一个class格式文件的开头4个字节是CAFEBABE,虚拟机就认为是可以使用的Class文件。

    接下来是:
    图5:


    在这里插入图片描述

    代表Class文件版本号的,即编译该Class文件的JDK版本号。高版本的JDK可以向下兼容低版本的Class文件,但是不能运行更高版本的Class文件。看下我们的HelloWorld:
    图6:


    在这里插入图片描述

    图7:


    JDK版本号
    看到版本号就是对应JDK1.8,没错,我确实是用JDK1.8编译的。

    咱继续往下走:
    图8:


    在这里插入图片描述

    接下来两个字节表示常量个数,对应的字节码:
    图9:


    在这里插入图片描述

    可以看到这里的常量有16(十进制22)个——然而实际上是十进制21个,这里是比较特殊的规定,将第0个常量空出来,是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义。

    常量池主要储存两大类常量:字面量和符号引用。字面量就是我们在程序中定义的常量,入字符串或者其他的final常量。符号引用就是类、接口、字段、方法等名称,它们会在虚拟机加载Class文件的时候进行动态链接,将符号引用转化为内存地,也就是将单纯的类名方法名等转化为虚拟机可以寻找到的内存地址,然后才可以执行程序逻辑。

    接下来是真正的主角常量池了:
    图10:


    在这里插入图片描述

    注意这里个数是constant_pool_count-1,也就对应了上面说的22-1=21个常量。这里每个常量都会用一个cp_info的表去表示。

    cp_info的通用结构:
    图11:


    cp_info的通用结构

    第一个字节是标志,因为Java虚拟机规范中一共有14种常量的表类型,所以标志是为了区分不同的常量表类型,如图:
    图12:


    14种常量的表类型

    看下我们的字节码对应的tag值:
    图13:


    在这里插入图片描述

    查一下常量项结构总表:
    图14:


    在这里插入图片描述
    我们的字节码tag这个字节是0A(十进制10),对应常量的表类型的:
    图15: 在这里插入图片描述

    即方法引用类型,表示这是代表一个方法的表:
    图16:


    在这里插入图片描述

    这里的方法引用代表其实就是两个index对应的常量合并起来,而index指的是常量在常量池中对应的位置(索引,也可以理解为偏移量,说白了就是第几个常量)。
    tag已经看过,现在看下第一个index,即字节码后面两个字节:
    图17:

    在这里插入图片描述

    03,也就是说方法的类描述符是第3个常量。
    先看再后面两个字节码:
    图18:


    在这里插入图片描述

    13(十进制19),也就是说方法名称和类型描述符指向第19个常量。
    额,才刚开始第一个,就要看到第19个。。

    没事,我早已经偷偷看完了整个常量池,已经算出第3个和第19个常量为常量项结构总表
    (图14)中的Constant_utf8_info,即UTF-8缩略编码的字符串(终于看到熟悉的身影了,后面马上就会讲解),第3个常量为 “java/lang/Object”,第19个常量是“< init>:()V”,组合起来就是“ java/lang/Object < init>:()V”,就是Object的init方法.也就是默认构造方法,因为我们的代码没有显式写构造方法。

    常量池的第一个常量到此结束,开始看第二个:
    先看下一个字节,即常量的tag:
    图19:


    在这里插入图片描述

    查下图14常量项结构总表tag为7的常量类型:
    图20:


    在这里插入图片描述
    这是一个Class info的常量,顾名思义就是指向类名的,看下接下来的两个表示index的字节:
    图21:
    在这里插入图片描述

    0014,即十进制20,也就是是常量池的第20个常量,然而我既然已经偷偷得看完了整个常量池表,就直接说了,第20个常量为utf-8,是“HelloWorld”,正是我们的源文件的类名~

    同理可以得到第3个常量的字节码为:
    图21:


    在这里插入图片描述

    也是Constant_Class_info类型,同上也可以得到对应的值为“java/lang/Object”。

    接下来看第4个常量的tag:
    图22:


    在这里插入图片描述

    查下图14常量项结构总表tag为7的常量类型:

    图23: 在这里插入图片描述
    刚才出现过的CONSTANT_Utf8_info类型,就是字符串本身(不用再去找其他字符串的感觉真好~)

    所谓的CONSTANT_Utf8_info,就是UTF-8缩略编码。在 Class 文件中,字符串是使用UTF-8缩略编码进行编码的。所谓的UTF-8缩略编码,就是‘\u0001-\u007f’(ASCII码从1-127的字符)用一个字节表示,'\u0080'-'\u07ff'之间的字符用两个字节表示,从‘\u0800’-'\uffff'所有字符就按照普通的UTF-8编码规则进行。可以看到图中每个字节都是在‘\u0001-\u007f’之间的,所以和ASCII码一样,整个转化过来就是“ < init>”。

    看下接下来代表字符串长度的2个字节:
    图24:


    在这里插入图片描述

    字符串长度为6个字节,看下接下来6个字节数据:
    图25:


    在这里插入图片描述

    后面一直到第18个常量都是CONSTANT_Utf8_info类型。
    根据上面的规则,可以看出第5个常量为“()V”:
    图26:


    在这里插入图片描述

    第6个常量为“Code”:

    图27: 在这里插入图片描述

    第7个常量为“LineNumberTable”:

    图28: 在这里插入图片描述 第8个常量为“LocalVariableTable”:

    图29:


    在这里插入图片描述
    第9个常量为“this”:
    图30:
    在这里插入图片描述

    第10个常量为“ LHelloWorld;”:
    图31:


    在这里插入图片描述

    第11个常量为“main ”:
    图32:


    在这里插入图片描述
    第12个常量为“([Ljava/lang/String;)V ”:
    图32:
    在这里插入图片描述

    第13个常量为“args ”:
    图34:


    在这里插入图片描述

    第14个常量为“[Ljava/lang/String;”:
    图35:


    在这里插入图片描述

    第15个常量为“a”:
    图28:36


    在这里插入图片描述

    第16个常量为“I”:
    图37:


    在这里插入图片描述

    第17个常量为“SourceFile”:
    图38:


    在这里插入图片描述
    第18个常量为“ HelloWorld.java”:
    图39:
    在这里插入图片描述

    第19个常量要注意下,tag为0C:
    图40:


    在这里插入图片描述

    对应的常量类型:
    图41:


    在这里插入图片描述

    表示字段或者方法名称,看下后面4个字节数据:
    图42:


    在这里插入图片描述

    也就是说对应的是第4和第5个常量,根据前面分析,第4个第5个常量分别是“< init>”和“()V”,即第19个常量为“< init>:()V”。

    第20个常量也是CONSTANT_Utf8_info类型,为“HelloWorld”:
    图43:

    在这里插入图片描述
    第21个常量为“ java/lang/Object”
    图44:
    在这里插入图片描述
    经过了这一轮略显疯狂的观察分析,21个常量总算看完了,感觉像是打败了一个大boss,这的确是看字节码最复杂麻烦的一块。而常量池分析完之后,它的意义就是后面的类还有字段方法等的名字等的索引都会指向这一块。

    那么常量池之后呢?请继续看图2的Class的一个整体结构图:

    图45:


    在这里插入图片描述

    紧接着两个字节代表访问标志,用于识别一些类或者接口层次的访问信息,包括这个Classs是类还是接口,是否定义为public,是否定义为abstract,如果是类的话,是否生命为final等。各个标志的具体含义如下表:

    图46:

    在这里插入图片描述
    目前只定义了8个标志,字节码文件中的实际的访问标志位的两个字节为当前Class的属性对应的标志位的或运算结果。

    看我们的helloWorld.class:

    图47:


    在这里插入图片描述

    0021,很容易看出对应的图46的ACC_PUBLIC和ACC_SUPER,也就是说Class的访问权限是public,并且可以使用invokespecial字节指令的新语义。

    而我们的HelloWorld正是public的,而ACC_SUPER正如图46所说,JDK1.2之后编译的Class文件都会有这个标志。

    继续往前走,还是看图2的Class的一个整体结构图:

    图48:


    在这里插入图片描述

    接下来四个字节就是类索引和父类索引以及接口索引数量,它们都是u2类型的数据(即都为2个字节),而接口索引集合是一组u2类型的集合,Class文件根据这三项数据来确定这个类的继承关系。类索引和父类索引都是用来确定其全限定名。

    Hello.World的字节码:

    图49:


    在这里插入图片描述

    可以看出当前类索引为0002(十进制2),父类索引为0003(十进制3),对应常量池具体的常量分比为:

    “HelloWorld”和“java/lang/Object”,确认过眼神,类名确实无误。

    接口索引数量为0,说明没有实现接口,Hello.World确实如此。

    继续前进,还是看图2的Class的一个整体结构图:

    图50:


    在这里插入图片描述

    开始进入字段的领域了。

    先观摩下Hello.World的字节码:

    图51:


    在这里插入图片描述

    很遗憾,我们的Hello.World简单到还没有字段。

    继续前进,还是看图2的Class的一个整体结构图:

    图52:


    在这里插入图片描述

    这是开始方法的领土:

    先看2个字节的方法数量:
    图53:


    在这里插入图片描述

    2个方法。咦?话说不是只有一个main方法么?别忘了还有默认构造方法啊。

    接下来自然就是具体的方法表,一个方法表表示一个方法,一个方法表的结构是这样的:

    图54:


    在这里插入图片描述

    所以我们先继续往前看8个字节:

    图55:


    在这里插入图片描述

    对应图54的access_flag为0001,查看access_flag表:
    图56:


    在这里插入图片描述

    access_flag为ACC_PUBLIC,name_index指向常量池第四个常量,即为“< init >“,表示构造方法。

    descriptor_index为0005,指向常量池第五个常量,即为 “()V”,说明该方法没有参数和返回值。

    attribute_count为0001,,表示该方法有一个属性表。
    何为属性表呢?可以理解为一个Class文件、字段表、方法表通过自己的属性表,描述某些场景的专有信息。可以理解为一些详细的描述信息。

    一个属性表的通用格式:

    图57:


    在这里插入图片描述

    具体的属性表有多种类型,这里通用格式的头两个字节attribute_name_index表示属性表名字索引,也就是确定具体属性表的名字。接下来四个字节attribute_length表示属性表的长度(准确来说是整个属性表长度减去attribute_name_index和attribute_length的长度),

    info就是具体的属性表了。

    看我们的Hello.World字节码:

    图58:


    在这里插入图片描述

    可得attribute_name_index为0006,对应常量池第6个常量,即为“Code”,说明这是Code属性表,也就是方法具体的代码。
    attribute_length为0000002f(十进制47),说明整个属性表除去attribute_name_index和attribute_length,长度为47个字节。

    关键看下Code属性表:

    图59:


    在这里插入图片描述

    这里前两个字段刚才已经说完了,我们从max_stack开始说起。

    先看接下来4个字节:

    图60:


    在这里插入图片描述

    可得max_stack为0001,即操作数栈最大深度值(关于操作数栈属于虚拟机执行引擎部分,这里就不作详细介绍了)

    max_locals为0001,说明局部变量表需要一个Slot的空间。Slot是虚拟机为局部变量分配内存所使用的最小单位。

    接下来是即code_length和code code_length和code存储java源程序编译后生成的字节码指令code_length代表字节码长度,code表示字节码指令的字节流

    看接下来4个字节,即code_length。

    图61:


    在这里插入图片描述

    可得code_length为00000005(十进制5),即有5个字节的字节码指令。

    查下Hello.World接下来的5个字节数据:
    图62:


    在这里插入图片描述

    查看字节码指令表:
    2A表示aload_0:将局部变量表的第0个Slot中为reference类型的本地变量推送到操作数栈顶

    B7表示invokespecial:这条指令的作用是以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪一个方法,它指向常量池中的一个CONSTANT_Methodref_info类型常量,即此方法的方法符号引用。所以接下来的0001就是表示invokespecial方法的符号引用,指向常量池第一个常量,即“java/lang/Object."< init >":()V”,也就是调用当前类的默认构造方法。

    B1表示return,含义是返回此方法,返回值为void。

    简单来说,这里就是将this压入操作数栈,然后调用它的构造方法,最后返回方法。

    接着 2 个字节为异常表长度:

    图63:


    在这里插入图片描述

    这里表示没有异常表数据。那么接下来也就不会有异常表的值。

    接着 2 个字节为方法本身的属性表长度:
    图64:


    在这里插入图片描述

    说明该方法本身有2个属性。

    属性表结构:
    图64:


    在这里插入图片描述

    和之前的一样,还是2个字节的名字索引还有4个字节的长度:

    图65: 在这里插入图片描述
    这里表示第一个属性的名称索引指向了常量池第7(0007)个常量,即“LineNumberTable”,属性的长度为6(00000006)个字节。
    LineNumberTable表示Java源代码和字节码行号之间的对应关系。
    具体结构:

    图65:


    在这里插入图片描述
    所以接下来还是看下面的2个字节:
    图66:
    在这里插入图片描述
    可见line_number_table的长度为1,说明接着跟着 1 个 line_number_info 类型的数据。

    具体的line_number_info 表:

    其包含了 start_pc 和 line_number 两个 u2 类型的数据项。前者是字节码行号,后者是 Java 源码行号。
    图65:


    在这里插入图片描述

    图66:


    在这里插入图片描述

    用来描述java源文件行号与字节码文件偏移量之间的对应关系。当程序运行抛出异常时,异常堆栈中显示出错的行号就是根据这个对应关系来显示的。可见start_pc为0000,line_number为0001。

    看完了方法第一个属性,再看方法的第二个属性:
    图67:


    在这里插入图片描述

    前2个字节表示属性名索引为0008,为常量池第8个常量,为“LocalVariableTable”,说明该属性为LocalVariableTable。

    后面4个字节表示该属性长度为0c(十进制12),即12个字节。

    具体的LocalVariableTable结构:
    图68:


    在这里插入图片描述

    分析方法也和LineNumberTable类似。LocalVariableTable用于描述栈帧中的局部变量表的变量和Java源码中定义的变量之间的关系。

    按照规则,Class文件中接下来的2个字节就是llocal_variable_table_length:
    图69:


    在这里插入图片描述

    llocal_variable_table_lengthe为1,即只有一个局部变量的表。

    其中local_variable_info结构如下:

    图70:


    在这里插入图片描述

    start_pc和length代表这个局部变量的生命周期开始的字节码偏移量及其作用范围的长度,合起来就是局部变量在字节码中的作用域。
    name_index和descriptor_index都为指向常量池的CONSTANT_Utf8_info索引,代表了这个局部变量的名称和描述符。
    index表示这个变量在局部变量表的位置(第几个Slot)。

    来看下Class文件中这几个值:
    图71:


    在这里插入图片描述

    start_pc为0000,length为0005。
    name_index为0009,即第9个常量“this”,所以该局部变量为this,这也符合默认构造方法隐含this为局部变量的规则。
    descriptor_index为000A,即第10个常量“LHelloWorld;”,这是方法的描述符。
    index为0000,表示该局部变量在局部变量表的第0个Slot位置。

    啊,一个默认默认构造方法终于讲完了,是不是有种如释重负的感觉呢?然而还有另一个方法main啊,不过用基本类似的方式看就可以了。

    方法字节码的分析,会不会感觉就如同洋葱那样“一层一层地剥落”呢?也许你可能有点分不清已经进入到第几层了,我从网上找来两张图就可以很清晰地看出整体结构:

    图72:
    来自 https://blog.csdn.net/zhangjg_blog/article/details/22432599

    在这里插入图片描述
    接下来看main方法就几乎一样了:
    图73:
    在这里插入图片描述
    查图56可得0009对应的access_flag为ACC_PUBLI和ACC_STATIC,即public static方法。

    name_index为000B,即常量池中第11个常量:“main”。
    descriptor_index为000c,即常量池中第12个常量:"([Ljava/lang/String;)V",即参数为String数组,返回为void的方法。
    attribute_count为0001,即有一个方法属性。

    属性表根据图59查看:
    图74:


    在这里插入图片描述

    attribute_name_index为0006,指向常量池第6个常量"Code",说明是Code属性。
    attribute_length为0000003B,即属性表长度为59。
    max_stack为0001,即操作数栈深度为1,。
    max_locals为0002,即局部变量表大小为2个Slot。
    code_length为00000003,即字节码指令为3个字节(3个指令)。

    3个指令分别为:、
    05:iconst_2:将int型常量2推送带操作数栈栈顶
    3c:istore_1:将操作数栈顶的int型数值存入局部变量表的第2个位置
    B1:return

    可以看出,代码中的1+1在编译时已经被替换为常量2了,然后赋值给局部变量表的第2个变量a。

    接下来就还是0个exception_table:
    图75:


    在这里插入图片描述

    方法属性还是2个:
    图76:


    在这里插入图片描述
    先看属性名称索引:
    图77:
    在这里插入图片描述

    即常量池第7个:“LineNumberTable”

    根据图65,先看接下来6个字节:
    图78:


    在这里插入图片描述

    attribute_length为000000A(10)字节,line_number_table_length为0002(2),说明有两个line_number_table,所以往下看2*4个字节:

    图79:


    在这里插入图片描述

    根据图65,可得:
    第1个line_number_table为:start_pc:0,即 start_pc 表示的字节码行号为第 0 行。line_number 为4,即 line_number 表示 Java 源码行号为第 4 行。
    第2个line_number_table为:start_pc:2,即 start_pc 表示的字节码行号为第 2行。line_number 为5,即 line_number 表示 Java 源码行号为第 5行。

    能看到这里的人我相信毅力和耐心已经很强悍了,当然我能写到这里毅力就更强悍了哈哈。

    再看下一个属性:
    图80:


    在这里插入图片描述

    前2个字节表示属性名索引为0008,为常量池第8个常量,为“LocalVariableTable”,说明该属性为LocalVariableTable。

    后面4个字节表示该属性长度为00000016(十进制22),即属性长度22个字节。
    根据图68看下接下来2个字节:
    图81:


    在这里插入图片描述

    local_variable_table_length为2,即有2个局部变量。
    根据图70查看接下来20个字节:

    图82:


    在这里插入图片描述

    前10个字节代表第1个局部变量:
    start_pc为0000,length为0003,表示了该局部变量在字节码的作用范围。
    name_index为000D,即常量池第13个常量,即" args",即局部变量名为args。descriptor_index为000E,即常量池第14个常量:"[Ljava/lang/String;",即变量描述符,说明该变量为String数组。
    index为0000,即变量在局部变量表的第0个Slot中。

    后10个字节第2个局部变量:
    start_pc为0002,length为0001,表示了该局部变量在字节码的作用范围。
    name_index为000F,即常量池第15个常量,即" args",即局部变量名为a。descriptor_index为0010,即常量池第16个常量:"I",即变量描述符,说明该变量为int类型。
    index为0001,即变量在局部变量表的第1个Slot中。
    这也与Java代码中的局部变量a相对应。

    main方法的LocalVariableTable到此结束,整个main方法也结束了~~

    看下总地图图2,我们已经来到最后一关:属性表集合
    注意,这是类的属性表,刚才说的是方法的属性表。

    图83:


    在这里插入图片描述

    图84:


    在这里插入图片描述

    说明该类只有一个属性。
    这是属性表的通用结构。
    图85:


    在这里插入图片描述
    所以看下接下来6个字节:
    图86:
    在这里插入图片描述

    attribute_name_index为0011(十进制17),即指向常量池第17个常量"SourceFile",说明它是SourceFile属性,用来记录类文件的名称的属性。
    attribute_length为00000002,即2字节数据。

    SourceFile属性的具体结构如图图87:


    在这里插入图片描述

    所以sourcefile_index为0012,即常量池低18个常量" HelloWorld.java"。
    这也和我们的Jav源文件名相同。

    结束语:

    到此为止,仿佛一场极为漫长的马拉松。剖析Class文件其实并不难,主要还是根据规范找到每个字节对应的含义。但是它是一个很消耗时间和精神的工作,因为需要用很大的耐心和细心去面对。当然,实际工作中肯定不需要如此繁琐乏味地去研读一个二进制流,有经验的工程师应该都会知道通过javap指令就可以反编译一个Class文件。使用javap -verbose HelloWorld.java,同样可以得到反编译结果:

    D:\Study\IdeaProjects\JvmClassDemo\out\production\JvmClassDemo>javap -verbose HelloWorld.class
    Classfile /D:/Study/IdeaProjects/JvmClassDemo/out/production/JvmClassDemo/HelloWorld.class
    Last modified 2019-8-28; size 397 bytes
    MD5 checksum fe279dd6838a18c4fc4cb208a4dd46ba
    Compiled from "HelloWorld.java"
    public class HelloWorld
    minor version: 0
    major version: 52
    flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:

    1 = Methodref #3.#19 // java/lang/Object."<init>": ()V

    2 = Class #20 // HelloWorld

    3 = Class #21 // java/lang/Object

    4 = Utf8 <init>

    5 = Utf8 ()V

    6 = Utf8 Code

    7 = Utf8 LineNumberTable

    8 = Utf8 LocalVariableTable

    9 = Utf8 this

    10 = Utf8 LHelloWorld;

    11 = Utf8 main

    12 = Utf8 ([Ljava/lang/String;)V

    13 = Utf8 args

    14 = Utf8 [Ljava/lang/String;

    15 = Utf8 a

    16 = Utf8 I

    17 = Utf8 SourceFile

    18 = Utf8 HelloWorld.java

    19 = NameAndType #4:#5 // "<init>": ()V

    20 = Utf8 HelloWorld

    21 = Utf8 java/lang/Object

    {
    public HelloWorld();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
    stack=1, locals=1, args_size=1
    0: aload_0
    1: invokespecial #1 // Method java/lang/Object."< init>": ()V
    4: return
    LineNumberTable:
    line 1: 0
    LocalVariableTable:
    Start Length Slot Name Signature
    0 5 0 this LHelloWorld;

    public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
    stack=1, locals=2, args_size=1
    0: iconst_2
    1: istore_1
    2: return
    LineNumberTable:
    line 4: 0
    line 5: 2
    LocalVariableTable:
    Start Length Slot Name Signature
    0 3 0 args [Ljava/lang/String;
    2 1 1 a I
    }
    SourceFile: "HelloWorld.java"

    很明显和上面的二进制流的分析是完全一致的,只是用了一种我们很容易看懂的方式去表达。

    既然有了javap这样的命令去反编译一个Class文件,那为何现在我还要用这么长的时间去写一个二进制文件的分析呢?

    我觉得有以下几点原因:

    1.只有真正去读一遍二进制文件,才可以真正去感受字节码文件的内在结构,才可以真正学到字节码的基本规范,这比单纯看文档解释字节码来得理解深入很多。
    2.而只有像1这样去深入理解,才可以对JVM的执行引擎和类加载机制融会贯通,串成系统的知识体系。
    3.真正读过一次字节码文件之后,才可以找到阅读其他的二进制文件的门路和感觉,而当你真正掌握了二进制流的结构之后,你就可以从很底层的地方去解决问题,去做一些有点神奇的“骚操作”。

    参考:
    《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版》
    Java虚拟机规范官网: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html

    相关文章

      网友评论

        本文标题:从字节码角度仔细剖析一个HelloWorld程序

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