字节码相关知识

149 阅读7分钟

1 学习目标

  • 什么是字节码
  • 字节码的结构
  • 如何查看字节码

2 什么是字节码

  • 是一种包含执行程序、由一序列 op 代码(操作码)/数据对组成的二进制文件,字节流组成生成的.class文件,虚拟机的文件格式,字节码是一种中间码,便于跨平台运行。

3 Class文件

  • Class文件是一组以8位字节为基础单位的二进制流,各项数据无添加分隔符严格按照顺序紧凑排布在Class文件之中。
  • 内部结构只包含两种数据结构:(view和ViewGroup理解)
    • 无符号整数(u%),类型u1u2u4分别表示无符号的一、二或四字节数量
    • (%_info),包含无符号整数或者其他表数据

截屏2022-10-25 17.58.56.png

官方文档声明的.class结构图,

ClassFile {
    u4             magic;               // 魔数,标致一个class文件
    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];  //属性数组
}

描述符

是表示字段或方法类型的字符串

例如类相关名称的叫法

* 类名:`java.lang.String`

* 类的内部名称:`java/lang/String`

* 类的类型描述符:`Ljava/lang/String;`

* 方法描述符:`([Ljava/lang/String)V`(比如`main`方法的描述符)

常量池

可以理解为Class文件的资源仓库,主要存放两大类常量

  • 字面量: 相当于语言级别的常量概念,如文本字符串、声明为final的常量值
  • 符号引用,编译原理方面的概念,三类常量
    • 类和接口的限定符
    • 字段的名称和描述符
    • 方法的名称和描述符

常量池下的表格数据都具有以下一般格式

cp_info {
    u1 tag;     // 标志位,代表哪类型的数据
    u1 info[];  // 根据标志位的不同,info数组的内容随标签的值而变化
}
常量类型                          |tag |
| ----------------------------- | -- |
| `CONSTANT_Class`              | 7  |    // 类或接口的符号引用
| `CONSTANT_Fieldref`           | 9  |    // 字段的符号引用
| `CONSTANT_Methodref`          | 10 |    //类中方法的符号引用
| `CONSTANT_InterfaceMethodref` | 11 |    //接口中方法符号引用
| `CONSTANT_String`             | 8  |    //字符串类型字面量
| `CONSTANT_Integer`            | 3  |
| `CONSTANT_Float`              | 4  |
| `CONSTANT_Long`               | 5  |
| `CONSTANT_Double`             | 6  |
| `CONSTANT_NameAndType`        | 12 |    //用于表示一个字段或方法,不指明它属于哪个类或接口类型
| `CONSTANT_Utf8`               | 1  |    // utf-8编码的字符串
| `CONSTANT_MethodHandle`       | 15 |   //表示方法句柄
| `CONSTANT_MethodType`         | 16 |    //标识方法类型
| `CONSTANT_InvokeDynamic`      | 18      //表示一个动态方法调用点`CONSTANT_Methodref` 为例子,  一个类方法的信息有什么; 1:属于哪个类 ,2:方法描述
CONSTANT_Methodref_info {
    u1 tag;                 // 这里的值是10
    u2 class_index;         // 指向一个tag7的值的位置
    u2 name_and_type_index; // 指向tag12的值的位置
}
CONSTANT_Class_info { 
    u1 tag;            // 这里的值是7
    u2 name_index;     // 指向一个tag1的值的字符串
}
CONSTANT_NameAndType_info {
    u1 tag;               // 这里的值是15
    u2 name_index;        // 指向一个tag1的值的字符串
    u2 descriptor_index;  // 指向method_info方法表的位置
}

字段表

用来描述接口或者类中声明的变量 (不包含方法内部声明的局部变量)

  • 类级变量 (静态变量)
  • 实例变量
field_info {
    u2            // 访问标志;
    u2            // 名称索引;
    u2            // 描述符索引;
    u2            // 属性计数;
    attribute_info attributes[attributes_count];  //属性表数组
}

方法表

method_info {
    u2             // 访问标志;
    u2             // 名称索引
    u2             // 描述符索引  
    u2             // 属性计数
    attribute_info attributes[attributes_count]; //属性数组
}

属性表

Class文件字段表方法表可有携带自己的属性表集合,用于描述某些场景特有的信息

attribute_info {
    u2  // 属性名索引;
    u4  // 属性长度;
    u1 info[attribute_length];  属性数组
}

最重要的属性Code
Code属性是method_info结构的属性表中的一个变长属性

Code_attribute {
    u2 attribute_name_index; //属性名索引; 固定是Code
    u4 attribute_length;     //属性长度; 
    u2 max_stack;            //最大操作数栈深度;  虚拟机用来分配栈帧
    u2 max_locals;           //局部遍历表所需要的存储空间;
    u4 code_length;          // 字节码长度;
    u1 code[code_length];    //字节码指令数组
    u2 exception_table_length; 
    {   u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

截屏2022-10-25 18.03.46.png

4.查看字节码

public class ByteCodeTest {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}
  1. 编译:javac -g ByteCodeTest.java
  2. 查看字节码:javap -p -v ByteCodeTest.class
Last modified 20221010日; size 540 bytes
  MD5 checksum c3a263c41a090ffd88d4fb7f710e1676
  Compiled from "ByteCodeTest.java"
public class ByteCodeTest   //类名
  minor version: 0
  major version: 55
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER   //类修饰符
  this_class: #5                          // ByteCodeTest
  super_class: #6                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:            //常量值,字面量和符号引用
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // Hello World!
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // ByteCodeTest
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               LByteCodeTest;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               ByteCodeTest.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               Hello World!
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               ByteCodeTest
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
  public ByteCodeTest();
    descriptor: ()V          //构造方式描述符
    flags: (0x0001) ACC_PUBLIC  
    Code:                      //方法code属性,也是代码区
      // 操作数栈深度,占用的Slot(变量槽)的大小(long,double占2个,其余1个),方法参数
      stack=1, locals=1, args_size=1
      // 0表示aload_0指令在代码数组中的下标,aload_0表示加载第0个变量槽位置上的对象,压入操作数栈顶
         0: aload_0 
      // 以栈顶变量为接收者,调用父类构造方法
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return                //方法结束指令
      LineNumberTable:            //行号表     确定code数组的哪一部分对应于原始源文件中的给定行号。
        line 5: 0
      LocalVariableTable:        //局部变量表
        Start  Length  Slot  Name   Signature  
            0       5     0  this   LByteCodeTest;  // 作用范围0-4,变量槽下标0,变量名this,变量类型ByteCodeTest

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V          //带参数方法描述符
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      //// 操作数栈深度,占用的Slot(变量槽)的大小(long,double占2个,其余1个),方法参数
      stack=2, locals=1, args_size=1
      //获取静态变量,入操作数栈
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       //加载常量,入操作数栈
         3: ldc           #3                  // String Hello World!
       // 调用虚方法
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 7: 0
        line 8: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}

常用指令

  • 加载和存储指令
    • _load_:将一个局部变量加载到操作数栈
    • _store_:将一个常量加载到操作数栈
  • 访问指令
    • 类字段 :getstatic、putstatic
    • 成员变量:getfield、putfield
  • 方法调用和返回指令
    • invokevirtual:用于调用对象的成员方法,根据对象的实际类型进行分派,支持多态。
    • invokeinterface:用于调用接口方法,会在运行时搜索由特定对象实现的接口方法进行调用。
    • invokespecial:用于调用一些需要特殊处理的方法,包括构造方法、私有方法和父类方法。
    • invokestatic:用于调用静态方法。
    • invokedynamic:用于在运行时动态解析出调用点限定符所引用的方法,并执行。
  • 对象创建
    • 创建实例或数组: new,newarray,new_
  • 运算指令
    • 对两个操作数栈的值进行某种运算,并把结果重新存储操作数栈
  • 类型转换指令
  • 操作数栈管理指令
    • 栈顶一个或两个元素出栈 pop 或pop2
    • 复制栈顶一个或两个数值并将复制值或者双份重新入栈; dup_
    • 栈顶数值互换
  • 控制转移指令
  • 异常处理指令
  • 同步指令

工具

参考

官网文档
带你看明白class二进制文件!
Class文件十六进制背后的秘密
Java字节码学习笔记(二):Java字节码怎么看

基于寄存器与基于栈
基于栈的虚拟机 VS 基于寄存器的虚拟机