Java字节码文件结构剖析-从JVM角度读字节码(四)

1.字节码的整体结构

在这里插入图片描述
说明:

类型 名称 说明 长度
u4 magic 魔数,识别Class文件格式 4个字节
u2 minor_version 副版本号 2个字节
u2 major_version 主版本号 2个字节
u2 constant_pool_count 常量池计算器 2个字节
cp_info constant_pool 常量池 n个字节
u2 access_flags 访问标志 2个字节
u2 this_class 类索引 2个字节
u2 super_class 父类索引 2个字节
u2 interfaces_count 接口计数器 2个字节
u2 interfaces 接口索引集合 2个字节
u2 fields_count 字段个数 2个字节
field_info fields 字段集合 n个字节
u2 methods_count 方法计数器 2个字节
method_info methods 方法集合 n个字节
u2 attributes_count 附加属性计数器 2个字节
attribute_info attributes 附加属性集合 n个字

Class文件结构描述:

ClassFile {
          u4 magic;
          u2 minor_version;
          u2 major_version;
          u2 constant_pool_count;
          cp_info constant_pool[constant_pool_count-1];
          u2 access_flags;
          u2 this_class;
          u2 super_class;
          u2 interfaces_count;
          u2 interfaces[interfaces_count];
          u2 fields_count;
          field_info fields[fields_count];
          u2 methods_count;
          method_info methods[methods_count];
          u2 attributes_count;
          attribute_info attributes[attributes_count];

2.Java字节码2种数据类型

  • 字节数据直接量:这是基本的数据类型。共细分为:u1、u2、u4、u8四种,分别代表连续的1个字节、2个字节、4个字节、8个字节组成的整体数据
  • 表(数组):表是由多个基本数据或者其他表,按照既定顺序组成的大的数据集合。表是有结构的,它的结构体现在:组成表的成分所在的位置和顺序都是已经严格定义好的。

3.代码案例:

public class MyTest1 {
    private int a = 1;

    public MyTest1() {
    }

    public int getA() {
        return this.a;
    }

    public void setA(int a) {
        this.a = a;
    }
}

执行命令javap -c MyTest1.class反编译:

Compiled from "MyTest1.java"
public class com.jvm.test.byteclass.MyTest1 {
  public com.jvm.test.byteclass.MyTest1(); // 构造方法
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_1
       6: putfield      #2                  // Field a:I
       9: return

  public int getA();  // getA方法
    Code:
       0: aload_0   load
       1: getfield      #2                  // Field a:I
       4: ireturn

  public void setA(int);
    Code:
       0: aload_0
       1: iload_1
       2: putfield      #2                  // Field a:I
       5: return
}

执行命令javap -verbose MyTest1.class获取更详细的信息:

Classfile /Users/zhengyunwei/Documents/code/PingPong/selleros/selleros-demo/jvm-demo/target/classes/com/jvm/test/byteclass/MyTest1.class // 文件的位置
  Last modified 2020-3-1; size 491 bytes // 最后修改时间,占用字节大小
  MD5 checksum 26e935bab4c89b0f4ee9ba147467c653 //md5
  Compiled from "MyTest1.java" // 编译源文件

 真正的文件编译结果:
 
public class com.jvm.test.byteclass.MyTest1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#21         // com/jvm/test/byteclass/MyTest1.a:I
   #3 = Class              #22            // com/jvm/test/byteclass/MyTest1
   #4 = Class              #23            // java/lang/Object
   #5 = Utf8               a
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/jvm/test/byteclass/MyTest1;
  #14 = Utf8               getA
  #15 = Utf8               ()I
  #16 = Utf8               setA
  #17 = Utf8               (I)V
  #18 = Utf8               SourceFile
  #19 = Utf8               MyTest1.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = NameAndType        #5:#6          // a:I
  #22 = Utf8               com/jvm/test/byteclass/MyTest1
  #23 = Utf8               java/lang/Object
{
  public com.jvm.test.byteclass.MyTest1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field a:I
         9: return
      LineNumberTable:
        line 10: 0
        line 13: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/jvm/test/byteclass/MyTest1;

  public int getA();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field a:I
         4: ireturn
      LineNumberTable:
        line 16: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/jvm/test/byteclass/MyTest1;

  public void setA(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #2                  // Field a:I
         5: return
      LineNumberTable:
        line 20: 0
        line 21: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/jvm/test/byteclass/MyTest1;
            0       6     1     a   I
}
SourceFile: "MyTest1.java"

class文件16进制解析为(推荐下载Hex Fiend,然后class文件):

 CA FE BA BE  00 00 00 34 00 18 0A 00 04 00 14 09 00 03 00 15 07 00 16 07 00 17 01 00 01 61 01 00 01 49
 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 
 72 54 61 62 6C 65 01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65 01 00 04 74 68 69 73 
 01 00 20 4C 63 6F 6D 2F 6A 76 6D 2F 74 65 73 74 2F 62 79 74 65 63 6C 61 73 73 2F 4D 79 54 65 73 74 31 
 3B 01 00 04 67 65 74 41 01 00 03 28 29 49 01 00 04 73 65 74 41 01 00 04 28 49 29 56 01 00 0A 53 6F 75 72 
 63 65 46 69 6C 65 01 00 0C 4D 79 54 65 73 74 31 2E 6A 61 76 61 0C 00 07 00 08 0C 00 05 00 06 01 00 1E 
 63 6F 6D 2F 6A 76 6D 2F 74 65 73 74 2F 62 79 74 65 63 6C 61 73 73 2F 4D 79 54 65 73 74 31 01 00 10 6A 
 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 00 21 00 03 00 04 00 00 00 01 00 02 00 05 00 06 00 00 00 
 03 00 01 00 07 00 08 00 01 00 09 00 00 00 38 00 02 00 01 00 00 00 0A 2A B7 00 01 2A 04 B5 00 02 B1 00 
 00 00 02 00 0A 00 00 00 0A 00 02 00 00 00 0A 00 04 00 0D 00 0B 00 00 00 0C 00 01 00 00 00 0A 00 0C 00 
 0D 00 00 00 01 00 0E 00 0F 00 01 00 09 00 00 00 2F 00 01 00 01 00 00 00 05 2A B4 00 02 AC 00 00 00 02 
 00 0A 00 00 00 06 00 01 00 00 00 10 00 0B 00 00 00 0C 00 01 00 00 00 05 00 0C 00 0D 00 00 00 01 00 10 
 00 11 00 01 00 09 00 00 00 3E 00 02 00 02 00 00 00 06 2A 1B B5 00 02 B1 00 00 00 02 00 0A 00 00 00 0A 
 00 02 00 00 00 14 00 05 00 15 00 0B 00 00 00 16 00 02 00 00 00 06 00 0C 00 0D 00 00 00 00 00 06 00 05 
 00 06 00 01 00 01 00 12 00 00 00 02 00 13

4.常量池中数据类型的结构总表:

在这里插入图片描述


5.字节码分析重要说明:
  • 所有class文件的16进制前四个字节CA FE BA BE为魔数,是固定的
  • 魔数CA FE BA BE后面四个字节00 00 00 34为版本号信息,前两个字节为次版本号( minor version: 0)0,后面两个字节为主版本号(major version:52)3*16+4=52即对应我们java的版本号为:1.8.0
  • 紧紧接着主版本号之后的就是常量池入口,常量池长度是不确定的。一个java类中定义的很多信息都是由常量池来维护和描述的(占的比重比较大的部分),可以将常量池看做一个class文件资源的仓库,比如说java中定义的方法和变量信息,都是存储在常量池中,常量池中主要存储两类常量:字面量和符号引用。字面量如文本字符串,java中声明为final的常量值等,而符号引用如类和接口的全限定名,字段名称和描述符,方法的名称和描述符等
  • 常量池的总体结构:java类所对应的常量池主要由常量池数量与常量池数组(常量表)这两部分共同构成。常量池数量紧跟在主版本号后面,占2个字节,常量池数组紧跟在常量池长度之后。常量池数组与一般的数组不同的是,常量池数组中不同的元素类型、结构都是不同的,但是每一种元素的第一个数据都是u1类型,该字节是一个标志位,占据1个字节。JVM在解析常量池时,就会根据这个u1类型来获取元素的具体类型。
    注意: 常量池数组中元素的个数=常量池数-1(其中0暂时不使用),目的是满足某些常量池索引值的数据在特定情况下需要表达不引用任何一个常量池的含义,根本原因是,索引0也是一个常量(JVM的保留常量,只不过它不位于常量表中,这个常量就对应null值),所以常量池的索引从1开始而非0开始。
  • 在JVM当中,每个变量/字段都是有描述信息的,主要作用是描述字段的数据类型、方法参数列表(包括数量、类型和顺序)与返回值。根据数据描述规则基本数据类型和代表无返回值的void类型都是用一个大写字符来表示,对象类型则是用L加一个对象的全限定名来表示。为了压缩字节码的体积,对于基本数据类型JVM都只使用一个大写字母来表示,例如:B-byte,C-char,D-double,F-float,I-int,J-long,S-short,Z-boolean,V-void;L对应的对象类型如:Ljava/lang/String;
  • 对于数组类型来说,每一个维度使用一个前置的[来表示,如int[] 被记录为[I, String[][] 被记录为[[L/java/lang/String;
  • 用描述符来描述方法的时候,按照先参数列表,后返回值的顺序来描述,参数列表按照参数的严格顺序放在一组()之内,如方法String getRealNameByIdAndName(int id,String name)的描述为:(I,Ljava/lang/String) Ljava/lang/String。

6.常量池字节码分析:

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

  • CA FE BA BE 魔数是固定的

  • 00 00 主版本号 对应描述符:minor version: 0

  • 00 34 次版本号 对应描述符: major version: 52

  • 00 18 常量池的长度描述-> 16+8-1= 23 有23个常量池描述符,0空置,为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达不引用任何一个常量池项目的含义,这种情况下就可以把索引值置为0来表示。由常量池的描述符:

    扫描二维码关注公众号,回复: 11051681 查看本文章
       #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V 
                              .
                              .
                              .
       #23 = Utf8               java/lang/Object
    

    可以看出常量池长度为23,索引从#1开始。

  • 常量池第一个常量描述 0A 00 04 00 14 表示:0A->标志位:10 (常量池中数据类型的结构总表)查询对应CONSTANT_Methodref_info,00 04->(指向声明方法的类描述符CONSTANT_Class_info的索引项)索引为#4,00 14 ->(指向方法名称及类型描述符CONSTANT_NameAndType_info的索引项)索引值为#20,字节码对应描述为:#1 = Methodref #4.#20 // java/lang/Object."<init>":()V,引用#4(#4 = Class #23 // java/lang/Object,引用#23(#23 = Utf8 java/lang/Object)),#20(#20 = NameAndType #7:#8 // "<init>":()V ) ,#20 应用了 #7(#7 = Utf8 <init>),#8( #8 = Utf8 ()V) 。所以最终的整体描述为:java/lang/Object."<init>":()V

  • 常量池第二个描述 09 00 03 00 15 ,其中09 查询常量池中数据类型的结构总表对应CONSTANT_Fieldref_info, 后两个字节 00 03 表示指向声明字段的类或接口描述符CONSTANT_Class_info的索引项 #3,00 15 指向字段名称及类型描述符CONSTANT_NameAndType_info的索引项 #21,即:#2 = Fieldref #3.#21 // com/jvm/test/byteclass/MyTest1.a:I

  • 后续字节码的分析根据字节码和下面表格的对应关系依次类推。


7. 类或者接口的修饰符ACCESS_FLAG

标志名 标志值 标志含义 针对的对像
ACC_PUBLIC 0x0001 public类型 所有类型
ACC_PRIVATE 0x000 private类型 所有类型
ACC_FINAL 0x0010 final类型
ACC_SUPER 0x0020 使用新的invokespecial语义 类和接口
ACC_INTERFACE 0x0200 接口类型 接口
ACC_ABSTRACT 0x0400 抽象类型 类和接口
ACC_SYNTHETIC 0x1000 该类不由用户代码生成 所有类型
ACC_ANNOTATION 0x2000 注解类型 注解
ACC_ENUM 0x4000 枚举类型 枚举

说明:
ACC_SUPER: invokespecial是一个字节码指令, 用于调用一个方法, 一般情况下, 调用构造方法或者使用super关键字显示调用父类的方法时, 会使用这条字节码指令。 这正是ACC_SUPER这个名字的由来。 在java 1.2之前, invokespecial对方法的调用都是静态绑定的, 而ACC_SUPER这个标志位在java 1.2的时候加入到class文件中, 它为invokespecial这条指令增加了动态绑定的功能。

示例字节码:
ACC_FLAG:常量池后两个字节表示类或者接口的修饰符,如示例代码中的16进制码中的0021,表示对应上述表格中的0x0020+0x0001即 ACC_SUPER 和 ACC_PUBLIC 也就是说是是个PUBLIC并且可以调用父类的方法的。示例中反编译flags: ACC_PUBLIC, ACC_SUPER
字节码对应图示:

在这里插入图片描述

反编译信息对应

在这里插入图片描述


8. 类名称和父类名称描述

  • This ClassName: ACC_FLAG后面2个字节表示当前类的名字即示例二进制码中的0003表示对应常量池中的#3 即#3 = Class #22 // com/jvm/test/byteclass/MyTest1
  • Super class name : This ClassName 后面两个字节表示父类的名称0004表示对应常量池的#4即:#4 = Class #23 // java/lang/Object

字节码图示:
在这里插入图片描述

在这里插入图片描述


9.接口个数和接口信息描述

Interface 接口个数(2个字节)+ 接口信息(n个字节)

  • 字节码图在这里插入图片描述
  • 字节码分析
    字节码中接口个数2个字节为0000表示接口个数为0个没有实现接口,所以后面实现接口的字节就没有了。

10.成员变量信息描述

  • field 成员变量 成员变量个数(2个字节)+ 成员变量信息(n个字节),字节码中表示成员变量个数的为01表示成员变量有一个即我们定义的int a字段

  • 字段表的结构:

    类型 名称 说明 数量
    u2 name_index 名称索引 1
    u2 access_flags 访问修饰符 1
    u2 name_index 名称索引 1
    u2 descriptor_index 描述符索引 1
    u2 attributes_count 属性表数量 1
    attribute_info attributes attributes_count n

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

  • 字段表字节码图示
    在这里插入图片描述

  • 变量表字节码分析:
    0001 表示成员表成员变量的个数为1
    0002 表示成员变量的方法描述对应 private。
    后面两个字节为变量名字,示例中11116进制码为0005对应class反编译#5索引即#5 = Utf8 a
    0006对应class反编译#6索引即#6 = Utf8 I
    0000表示成员变量属性数量此处为0
    接着后面两个字节为attributes_count属性个数,示例中对应的为0000 即属性个数为0, 也就是属性值信息不存在。


11.方法信息描述

方法表结构:

名称 类型 数量
access_flags u2 1
name_index u2 1
descriptor_index u2 1
attributes_count u2 1
attributes attribute_info attributes_count

在这里插入图片描述
在这里插入图片描述
attribute结构:

名称 类型 数量
attributes_name_index u2 1
attributes_name_length u4 1
descriptor_index u2 1
attributes_count u2 1
attribute_info u1 n

Code属性表的结构,如下图:

类型 名称 数量 含义
u2 attribute_name_index 1 属性名索引
u4 attribute_length 1 属性长度
u2 max_stack 1 操作数栈深度的最大值
u2 max_locals 1 局部变量表所需的存续空间
u4 code_length 1 字节码指令的长度
u1 code code_length 存储字节码指令
u2 exception_table_length 1 异常表长度
exception_info exception_table exception_length 异常表
u2 attributes_count 1 属性集合计数器
attribute_info attributes attributes_count 属性集合

Code_attribute作用是保存该方法的结构,下图为Code_attribute示意图:
在这里插入图片描述

  • attribute_length:表示attribute所包含的字节数,不包含attribute_name_index和attribute_length字段
  • max_stack:表示这个方法运行的任何时刻所能达到的操作数栈的最大深度
  • max_locals:表示方法执行期间创建的局部变量的容量,包含用来表示传入的参数的局部变量
  • code_length:表示该方法所包含的字节数以及具体的指令码
  • exception_table: 存放的是处理异常的信息,每个exception_table表项由start_pc,end_pc,hander_pc,catch_type组成。
  • start_pc和end_pc表示在code数组中从start_pc到end_pc(包含start_pc,不包含end_pc)的指令抛出的异常会由这个表项来处理
  • hander_pc表示处理异常的代码的开始处。catch_type表示会被处理的异常的类型,它执行常量池里的一个异常类。当catch_type为0时,表示处理所有的异常。

字节码示例构造器方法:
在这里插入图片描述
其中我们对code_length下的字节码指令详细分析,这边推荐一个idea插件jclasslib可以详细的看到字节码信息及其对应关系:
2A B7 00 01 2A 04 B5 00 02 B1 ( 指令)
在这里插入图片描述
​构造方法下的code指令为如图所示:
我们可以看出

  • 2A->aload_0,可以参考官方说明 访问地址:aload_0 = 42 (0x2a)
  • B7->invokespecial 调用父类的相应的构造方法,可以参考官方说明 访问地址:invokespecial = 183 (0xb7)
  • 0001->为B7的参数,对应常量池索引#1的内容:#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
  • 2A->aload_0
  • 04->iconst_1 可以参考官方说明 访问地址:iconst_1 = 4 (0x4)
  • B5->putfield,为相应的字段赋值,可以参考官方说明 访问地址:putfield = 181 (0xb5)
  • 0002->为B5的参数,指向常量池#2,即:#2 = Fieldref #3.#21 // com/jvm/test/byteclass/MyTest1.a:I
  • B1->return 可以参考官方说明 访问地址:return = 177 (0xb1)

大致流程分析如下:
在这里插入图片描述
LineNumberTable:
在这里插入图片描述
对应是个字节长度10个字节:00 02 00 00 00 0A 00 04 00 0D

  • 0002 表示存在两对映射,即0000 000A 为一对,00 04 00 0D为一对
  • 0000 000A 表示 0000映射到0003,字节码的偏移量为0,映射到字节码的行号为10
  • 00 04 00 0D 表示偏移量为4,映射到字节码行号为13

LocalVariableTable:
在这里插入图片描述
对应12个字节:00 01 00 00 00 0A 00 0C 00 0D 00 00

  • 0001 表示局部变量的个数 1 编译器隐式的传入当前对象This
  • 0000-000A 局部变量的开始到结束为止0-10
  • 000C 对应常量池#12 this 当前对象
  • 000D 对局部变量的描述对应常量池#13
  • 0000 StackMyTable从1.6加入,校验检查的。

其他两个方法getA和setA雷同构造器方法的分析方式。

发布了41 篇原创文章 · 获赞 14 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Yunwei_Zheng/article/details/104595572