一文让你明白Java字节码

作者: 黑马有点白986 | 来源:发表于2017-03-09 16:08 被阅读9956次

    也许你写了无数行的代码,也许你能非常溜的使用高级语言,但是你未必了解那些高级语言的执行过程。例如大行其道的Java。

    Java号称是一门“一次编译到处运行”的语言,但是我们对这句话的理解深度又有多少呢?从我们写的java文件到通过编译器编译成java字节码文件(也就是.class文件),这个过程是java编译过程;而我们的java虚拟机执行的就是字节码文件。不论该字节码文件来自何方,由哪种编译器编译,甚至是手写字节码文件,只要符合java虚拟机的规范,那么它就能够执行该字节码文件。那么本文主要讲讲java字节码文件相关知识。接下来我们通过具体的Demo来深入理解:

    1 首先我们来写一个java源文件

    javasrc.png

    上面是我们写的一个java程序,很简单,只有一个成员变量a以及一个方法testMethod() 。

    2 接下来我们用javac命令或者ide工具将该java源文件编译成java字节码文件。

    demo.png

    上图是编译好的字节码文件,我们可以看到一堆16进制的字节。如果你使用IDE去打开,也许看到的是已经被反编译的我们所熟悉的java代码,而这才是纯正的字节码,这也是我们今天需要讲的内容重点。

    也许你会对这样一堆字节码感到头疼,不过没关系,我们慢慢试着你看懂它,或许有不一样的收获。在开始之前我们先来看一张图

    java_byte.jpeg

    这张图是一张java字节码的总览图,我们也就是按照上面的顺序来对字节码进行解读的。一共含有10部分,包含魔数,版本号,常量池等等,接下来我们按照顺序一步一步解读。

    3.1 魔数

    从上面的总览图中我们知道前4个字节表示的是魔数,对应我们Demo的是 0XCAFE BABE。什么是魔数?魔数是用来区分文件类型的一种标志,一般都是用文件的前几个字节来表示。比如0XCAFE BABE表示的是class文件,那么有人会问,文件类型可以通过文件名后缀来判断啊?是的,但是文件名是可以修改的(包括后缀),那么为了保证文件的安全性,讲文件类型写在文件内部来保证不被篡改。
    从java的字节码文件类型我们看到,CAFE BABE翻译过来是咖啡宝贝之意,然后再看看java图标。

    java_icon.png

    CAFE BABE = 咖啡。

    3.2 版本号

    我们识别了文件类型之后,接下来要知道版本号。版本号含主版本号和次版本号,都是各占2个字节。在此Demo种为0X0000 0033。其中前面的0000是次版本号,后面的0033是主版本号。通过进制转换得到的是次版本号为0,主版本号为51。
    从oracle官方网站我们能够知道,51对应的正式jdk1.7,而其次版本为0,所以该文件的版本为1.7.0。如果需要验证,可以在用java --version命令输出版本号,或者修改编译目标版本--target重新编译,查看编译后的字节码文件版本号是否做了相应的修改。

    至此,我们共了解了前8字节的含义,下面讲讲常量池相关内容。

    3.3 常量池

    紧接着主版本号之后的就是常量池入口。常量池是Class文件中的资源仓库,在接下来的内容中我们会发现很多地方会涉及,如Class Name,Interfaces等。常量池中主要存储2大类常量:字面量和符号引用。字面量如文本字符串,java中声明为final的常量值等等,而符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符。

    为什么需要类和接口的全局限定名呢?系统引用类或者接口的时候不是通过内存地址进行操作吗?这里大家仔细想想,java虚拟机在没有将类加载到内存的时候根本都没有分配内存地址,也就不存在对内存的操作,所以java虚拟机首先需要将类加载到虚拟机中,那么这个过程设计对类的定位(需要加载A包下的B类,不能加载到别的包下面的别的类中),所以需要通过全局限定名来判别唯一性。这就是为什么叫做全局,限定的意思,也就是唯一性。

    在进行具体常量池分析之前,我们先来了解一下常量池的项目类型表:

    jvm_constant.png

    上面的表中描述了11中数据类型的结构,其实在jdk1.7之后又增加了3种(CONSTANT_MethodHandle_info,CONSTANT_MethodType_info以及CONSTANT_InvokeDynamic_info)。这样算起来一共是14种。接下来我们按照Demo的字节码进行逐一翻译。

    0x0015:由于常量池的数量不固定(n+2),所以需要在常量池的入口处放置一项u2类型的数据代表常量池数量。因此该16进制是21,表示有20项常量,索引范围为1~20。明明是21,为何是20呢?因为Class文件格式规定,设计者就讲第0项保留出来了,以备后患。从这里我们知道接下来我们需要翻译出20项常量。
    Constant #1 (一共有20个常量,这是第一个,以此类推...)
    0x0a-:从常量类型表中我们发现,第一个数据均是u1类型的tag,16进制的0a是十进制的10,对应表中的MethodRef_info。
    0x-00 04-:Class_info索引项#4
    0x-00 11-:NameAndType索引项#17
    Constant #2
    0x-09: FieldRef_info
    0x0003 :Class_info索引项#3
    0x0012:NameAndType索引项#18
    Constant #3
    0x07-: Class_info
    0x-00 13-: 全局限定名常量索引为#19
    Constant #4
    0x-07 :Class_info
    0x0014:全局限定名常量索引为#20
    Constant #5
    0x01:Utf-8_info
    0x-00 01-:字符串长度为1(选择接下来的一个字节长度转义)
    0x-61:"a"(十六进制转ASCII字符)
    Constant #6
    0x01:Utf-8_info
    0x-00 01:字符串长度为1
    0x-49:"I"
    Constant #7
    0x01:Utf-8_info
    0x-00 06:字符串长度为6
    0x-3c 696e 6974 3e-:"<init>"
    Constant #8
    0x01 :UTF-8_info
    0x0003:字符串长度为3
    0x2829 56:"()V"
    Constant #9
    0x-01:Utf-8_info
    0x0004:字符串长度为4
    0x436f 6465:"Code"
    Constant #10
    0x01:Utf-8_info
    0x00 0f:字符串长度为15
    0x4c 696e 654e 756d 6265 7254 6162 6c65:"LineNumberTable"
    Constant #11
    ox01: Utf-8_info
    0x00 12字符串长度为18
    0x-4c 6f63 616c 5661 7269 6162 6c65 5461 626c 65:"LocalVariableTable"
    Constant #12
    0x01:Utf-8_info
    0x0004 字符串长度为4
    0x7468 6973 :"this"
    Constant #13
    0x01:Utf-8_info
    0x0f:字符串长度为15
    0x4c 636f 6d2f 6465 6d6f 2f44 656d 6f3b:"Lcom/demo/Demo;"
    Constant #14
    0x01:Utf-8_info
    0x00 0a:字符串长度为10
    ox74 6573 744d 6574 686f 64:"testMethod"
    Constant #15
    0x01:Utf-8_info
    0x000a:字符串长度为10
    0x536f 7572 6365 4669 6c65 :"SourceFile"
    Constant #16
    0x01:Utf-8_info
    0x0009:字符串长度为9
    0x-44 656d 6f2e 6a61 7661 :"Demo.java"
    Constant #17
    0x0c :NameAndType_info
    0x0007:字段或者名字名称常量项索引#7
    0x0008:字段或者方法描述符常量索引#8
    Constant #18
    0x0c:NameAndType_info
    0x0005:字段或者名字名称常量项索引#5
    0x0006:字段或者方法描述符常量索引#6
    Constant #19
    0x01:Utf-8_info
    0x00 0d:字符串长度为13
    0x63 6f6d 2f64 656d 6f2f 4465 6d6f:"com/demo/Demo"
    Constant #20
    0x01:Utf-8_info
    0x00 10 :字符串长度为16
    0x6a 6176 612f 6c61 6e67 2f4f 626a 6563 74 :"java/lang/Object"
    到这里为止我们解析了所有的常量。接下来是解析访问标志位。

    3.4 Access_Flag 访问标志

    访问标志信息包括该Class文件是类还是接口,是否被定义成public,是否是abstract,如果是类,是否被声明成final。通过上面的源代码,我们知道该文件是类并且是public。

    access_flag.png

    0x 00 21:是0x0020和0x0001的并集。其中0x0020这个标志值涉及到了字节码指令,后期会有专题对字节码指令进行讲解。期待中......

    3.5 类索引

    类索引用于确定类的全限定名
    0x00 03 表示引用第3个常量,同时第3个常量引用第19个常量,查找得"com/demo/Demo"。#3.#19

    3.6父类索引

    0x00 04 同理:#4.#20(java/lang/Object)

    3.7 接口索引

    通过java_byte.jpeg图我们知道,这个接口有2+n个字节,前两个字节表示的是接口数量,后面跟着就是接口的表。我们这个类没有任何接口,所以应该是0000。果不其然,查找字节码文件得到的就是0000。

    3.8 字段表集合

    字段表用于描述类和接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量。
    同样,接下来就是2+n个字段属性。我们只有一个属性a,按道理应该是0001。查找文件果不其然是0001。
    那么接下来我们要针对这样的字段进行解析。附上字段表结构图

    字段表结构.png

    0x00 02 :访问标志为private(自行搜索字段访问标志)
    0x00 05 : 字段名称索引为#5,对应的是"a"
    0x 00 06 :描述符索引为#6,对应的是"I"
    0x 00 00 :属性表数量为0,因此没有属性表。
    tips:一些不太重要的表(字段,方法访问标志表)可以自行搜索,这里就不贴出来了,防止篇幅过大。

    3.9 方法

    我们只有一个方法testMethod,按照道理应该前2个字节是0001。通过查找发现是0x00 02。这是什么原因,这代表着有2个方法呢?且继续看......

    方法表结构.png

    上图是一张方法表结构图,按照这个图我们分析下面的字节码:

    第1个方法:

    0x00 01:访问标志 ACC_PUBLIC,表明该方法是public。(可自行搜索方法访问标志表)
    0x00 07:方法名索引为#7,对应的是"<init>"
    0x00 08:方法描述符索引为#8,对应的是"()V"
    0x00 01:属性表数量为1(一个属性表)
    那么这里涉及到了属性表。什么是属性表呢?可以这么理解,它是为了描述一些专有信息的,上面的方法带有一张属性表。所有属性表的结构如下图:
    一个u2的属性名称索引,一个u2的属性长度加上属性长度的info。
    虚拟机规范预定义的属性有很多,比如Code,LineNumberTable,LocalVariableTable,SourceFile等等,这个网上可以搜索到。

    属性表结构.png

    按照上面的表结构解析得到下面信息:
    0x0009:名称索引为#9("Code")。
    0x000 00038:属性长度为56字节。
    那么接下来解析一个Code属性表,按照下图解析

    code.png

    前面6个字节(名称索引2字节+属性长度4字节)已经解析过了,所以接下来就是解析剩下的56-6=50字节即可。
    0x00 02 :max_stack=2
    0x00 01 : max_locals=1
    0x00 0000 0a : code_length=10
    0x2a b700 012a 04b5 0002 b1 : 这是code代码,可以通过虚拟机字节码指令进行查找。
    2a=aload_0(将第一个引用变量推送到栈顶)
    b7=invokespecial(调用父类构造方法)
    00=什么都不做
    01 =将null推送到栈顶
    2a=同上
    04=iconst_1 将int型1推送到栈顶
    b5=putfield 为指定的类的实例变量赋值
    00= 同上
    02=iconst_m1 将int型-1推送栈顶
    b1=return 从当前方法返回void
    整理,去除无动作指令得到下面
    0 : aload_0
    1 : invokespecial
    4 : aload_0
    5 : iconst_1
    6 : putfield
    9 : return
    关于虚拟机字节码指令这块内容,后期会继续深入下去...... 目前只需要了解即可。接下来顺着Code属性表继续解析下去:
    0x00 00 : exception_table_length=0
    0x00 02 : attributes_count=2(Code属性表内部还含有2个属性表)
    0x00 0a: 第一个属性表是"LineNumberTable"

    LineNumberTable.png

    0x00 0000 0a : "属性长度为10"
    0x00 02 :line_number_table_length=2
    line_number_table是一个数量为line_number_table_length,类型为line_number_info的集合,line_number_info表包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号
    0x00 00 : start_pc =0
    0x00 03 : end_pc =3
    0x00 04 : start_pc=4
    0x00 04 : end_pc=4

    0x00 0b 第二个属性表是:"LocalVariableTable"

    local_variable_table.png local_variable_info.png

    0x00 0000 0c:属性长度为12
    0x00 01 : local_variable_table_length=1
    然后按照local_variable_info表结构进行解析:
    0x00 00 : start_pc=0
    0x00 0a:length=10
    0x000c : name_index="this"
    0x000d : descriptor_index #13 ("Lcom/demo/Demo")
    0000 index=0
    //-------到这里第一个方法就解析完成了-------//
    Method(<init>)--1个属性Code表-2个属性表(LineNumberTable ,LocalVariableTable)接下来解析第二个方法

    第2个方法:

    0x00 04:"protected"
    0x00 0e: #14("testMethod")
    0x00 08 : "()V"
    0x0001 : 属性数量=1
    0x0009 :"Code"
    0x0000 002b 属性长度为43
    解析一个Code表
    0000 :max_stack =0
    0001 : max_local =1
    0000 0001 : code_length =1
    0xb1 : return(该方法返回void)
    0x0000 异常表长度=0
    0x0002 属性表长度为2
    //第一个属性表
    0x000a : #10,LineNumberTable
    0x0000 0006 : 属性长度为6
    0x0001 : line_number_length = 1
    0x0000 : start_pc =0
    0x0008 : end_pc =8
    //第二个属性表
    0x000b : #11 ,LocalVariableTable
    0x0000 000c : 属性长度为12
    0x0001 : local_variable_table_length =1
    0x0000 :start_pc = 0
    0x0001: length = 1
    0x000c : name_index =#12 "this"
    0x000d : 描述索引#13 "Lcom/demo/Demo;"
    0000 index=0

    //到这里为止,方法解析都完成了,回过头看看顶部解析顺序图,我们接下来就要解析Attributes了。

    3.10 Attribute

    0x0001 :同样的,表示有1个Attributes了。
    0x000f : #15("SourceFile")
    0x0000 0002 attribute_length=2
    0x0010 : sourcefile_index = #16("Demo.java")
    SourceFile属性用来记录生成该Class文件的源码文件名称。

    source_file.jpeg

    4 另话

    其实,我们写了这么多确实很麻烦,不过这种过程自己体验一遍的所获所得还是不同的。现在,使用java自带的反编译器来解析字节码文件。
    javap -verbose Demo //不用带后缀.class


    javap_result.png

    5 总结

    到此为止,讲解完成了class文件的解析,这样以后我们也能看懂字节码文件了。了解class文件的结构对后面进一步了解虚拟机执行引擎非常重要,所以这是基础并重要的一步。

    6 工具

    https://github.com/zxh0/classpy 这是一款分析字节码文件的工具,挺好用。

    相关文章

      网友评论

      • MachineDora:那个……常量池那里没看懂呢。。。就大概知道魔数和版本和常量池后面和具体代码相关的,那个常量池到底什么意思啊?就是那种各种索引#数字这种的,有点迷。
        黑马有点白986:@刺雒 赞
        刺雒:常量池主要存储字面量和符号引用,字面量就是我们平时使用的String的值,final定义的常量值等。
        符号引用就是类名、方法名和描述符、字段名和描述符,描述符包括字段的类型,方法对应的参数列表和方法值等
      • Jerry_5652:很感谢!之前看我的书上很详细没看懂,然后看了你这个,再看看书,发现我能看懂书了!!
        黑马有点白986:谢谢支持
      • 半岛铁盒里的猫:感谢作者的分享。请问这方面的内容要系统学习有哪些书籍网站可以看?
        黑马有点白986:《深入理解Java虚拟机》,里面其实知道大概就行。
      • 23937b882031:一句一句对照《深入理解JVM》第6章看完了,透彻明了,赞:+1:
        黑马有点白986:感谢支持,欢迎多多交流
      • 4ee3e4efa6e1:Code属性表那部分中code的解析有误(比如:“00=什么都不做,01 =将null推送到栈顶”),操作码的结构(Format)是不一样的,b7,也就是invokespecial,结构是{invokespecial;indexbyte1;indexbyte2},由三个字节构成,包含三个部分,其中indexbyte1和indexbyte2共同构成 方法在run-time constant pool中的索引。
      • MYJDBC:学习了😊
      • 活着工作室:完全看不懂啊
        黑马有点白986:没关系,看看而已
      • zhouinfo:> 按照上面的表结构解析得到下面信息:
        0x0009:名称索引为#9("Code")。
        0x000 00038:属性长度为56字节。

        上面那个表 "属性表结构.png" 中的attribute_lenght的类型应该是u4吧
      • wust丶杰:厉害厉害
        黑马有点白986:@wust丶杰 过奖过奖
      • 43727794af82:写得真好
        黑马有点白986:感谢支持
      • dcdc87be3c57:老厉害了
        黑马有点白986:@theboundaryfore 感谢支持
      • cqxxxxxxxx:很棒
        黑马有点白986:@cqxxxxxxxx 感谢阅读,github上有工具可以分析字节码。
      • 526e29b579e5:为什么我用notepad++打开.class文件是一堆乱码,并没有数字,我记得之前打开过,忘了正确的打开方式了,请赐教
        N3verL4nd:notepad++使用HEX-Editor插件,或者其他的以16进制打开文件。
        黑马有点白986:也许是编码问题
      • 吴少在coding:这是jvm的内容?
        黑马有点白986:Jvm就是加载字节码的,如何不是?
      • 用语321:学习了谢谢
        黑马有点白986:@用语321 互相学习~~
      • AlexLD:一篇很精致的文章,加油
        黑马有点白986:谢谢你,共同进步!

      本文标题:一文让你明白Java字节码

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