Skip to content

JVM 字节码文件结构

本文主要介绍字节码文件(.class后缀结尾的文件)包括什么内容。

1. 基本结构

在Java开发中,我们编写的源代码保存在以.java结尾的源代码文件中,经过编译器javac编译后,生成字节码文件(.class结尾)。

在JVM规范中,字节码文件结构包含内容如下:

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];
}

总结可以分为如下内容:

  • 魔数
  • 字节码文件版本号
  • 常量池
  • 访问标志
  • 类索引、父类索引、接口索引集合
  • 字段表集合
  • 方法表集合
  • 属性表集合

字节码文件本质是字节流,每个字节紧密排列,没有分割符,所以在其中的数据项,不论是顺序还是数量,都是严格规定的。

字节码文件格式采用一种类似于C语言结构体的方式进行数据存储,这种结构只有两种数据类型:无符号数和表

  • 无符号数:属于基本数据类型,以u1、u2、u4、u8来分别代表1个、2个、4个、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或是按照UTF-8编码构成字符串值;
  • 表:是由多个无符号数或其他表作为数据项构成的复合数据类型,所有表都习惯性地以_info结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。由于表没有固定长度,所以通常会在其前面加上数量说明。

2. 魔数

每个字节码文件开头的4个字节的无符号整数称为魔数(Magic Number),魔数的唯一作用是确定字节码文件是否为合法有效的,即:魔数是字节码文件的标识。

魔数的固定值是0xCAFEBABE,不会改变。

3. 字节码文件版本号

紧接着魔数的4个字节存储的是字节码文件的版本号,同样也是4个字节。第5个和第6个字节所代表的含义就是编译的副版本号minor_version,第7个和第8个字节就是编译的主版本号major_version。

父版本号和主版本号共同构成了字节码文件的格式版本号,例如,主版本号为M,副版本号为m,那么格式版本号就是M.m

字节码文件版本号的主要作用是向后兼容,即高版本的Java虚拟机可以执行由低版本编译器生成的字节码文件,但是低版本的Java虚拟机不能执行由高版本编译器生成的字节码文件,否则JVM会抛出java.lang.UnsupportedClassVersionError异常。

下表显示了Java编译器版本及其生成的字节码文件版本,以及JVM虚拟机支持的字节码文件版本:

Java SEReleasedMajorSupported majors
1.0.2May 19964545
1.1February 19974545
1.2December 19984645 .. 46
1.3May 20004745 .. 47
1.4February 20024845 .. 48
5.0September 20044945 .. 49
6December 20065045 .. 50
7July 20115145 .. 51
8March 20145245 .. 52
9September 20175345 .. 53
10March 20185445 .. 54
11September 20185545 .. 55
12March 20195645 .. 56
13September 20195745 .. 57
14March 20205845 .. 58
15September 20205945 .. 59
16March 20216045 .. 60
17September 20216145 .. 61
18March 20226245 .. 62
19September 20226345 .. 63
20March 20236445 .. 64
21September 20236545 .. 65
22March 20246645 .. 66
23September 20246745 .. 67
24March 20256845 .. 68

4. 常量池

在字节码文件版本号之后,紧跟着的是常量池。由于常量池条目数量不定,所以首先是2个字节的常量池数量(constant_pool_count),然后是常量池表。

常量池数量值(constant_pool_count)为常量池表长度+1。

常量池中主要存放两大类常量:字面量和符号引用。

  • 字面量:具体的值包括文本字符串、声明为final的常量值;

  • 符号引用:具体内容包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符;

    • 全限定名:com/test/Demo就是类的全限定名,仅仅是把包名的.替换成/,为了使连续的多个全限定之间不产生混淆,在使用时,最后一般会加入一个;表示全限定名结束;

    • 简单名称:简单名称是指没有类型和参数修饰的方法或字段名称,例如方法add()的简单名称是add

    • 描述符:描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、short、int、long、float、double、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而引用类型则用字符L加全限定名来表示,如下表:

      标志符含义
      Bbyte
      Cchar
      Sshort
      Iint
      Jlong
      Ffloat
      Ddouble
      Zboolean
      Vvoid
      L引用类型,例如Ljava/lang/Object;
      [数组类型,例如int[] 表示为[idouble[][][]表示为[[[d

常量池表条目可以分为如下类型,以Tag区分:

Constant KindTagclass file formatJava SE描述
CONSTANT_Utf8145.31.0.2UTF-8编码的字符串
CONSTANT_Integer345.31.0.2整型字面量
CONSTANT_Float445.31.0.2浮点型字面量
CONSTANT_Long545.31.0.2长整型字面量
CONSTANT_Double645.31.0.2双精度浮点型字面量
CONSTANT_Class745.31.0.2类或接口的符号引用
CONSTANT_String845.31.0.2字符串类型符号引用
CONSTANT_Fieldref945.31.0.2字段的符号引用
CONSTANT_Methodref1045.31.0.2类中方法的符号引用
CONSTANT_InterfaceMethodref1145.31.0.2接口中方法的符号引用
CONSTANT_NameAndType1245.31.0.2名称和类型
CONSTANT_MethodHandle1551.07方法句柄
CONSTANT_MethodType1651.07方法类型
CONSTANT_Dynamic1755.011动态常量
CONSTANT_InvokeDynamic1851.07动态方法调用
CONSTANT_Module1953.09模块的符号引用
CONSTANT_Package2053.09包的符号引用

每种条目都有自己的结构,例如,CONSTANT_Utf8结构如下:

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}
  • 首先是1个字节的tag,标识常量池条目类型,CONSTANT_Utf8_info的值为1;
  • 然后是2个字节的长度length,表示字符串的字节长度;
  • 然后是长度为length值的字节数组,表示具体的字符串内容;

再例如,CONSTANT_Integer_info的结构如下:

CONSTANT_Integer_info {
    u1 tag;
    u4 bytes;
}
  • 首先是一个字节的tag,CONSTANT_Integer_info的值为3;
  • 然后是4个字节的无符号数,表示一个整数;

其他条目结构类似,此处不再赘述。

5. 访问标志

在常量池之后,紧跟着的是2个字节的访问标志。每个位上的值为1,代表该标志为真,不同位代表不同意思,下表列出了各个位的含义:

标志名称解释
ACC_PUBLIC0x0001是否为public的
ACC_FINAL0x0010是否为final的,不允许被继承
ACC_SUPER0x0020标志允许使用invokespecial字节码指令,JDK 1.0.2之后编译出来的类,这个标志默认为真
ACC_INTERFACE0x0200标志这是一个接口
ACC_ABSTRACT0x0400是否为abstract的,对于接口或抽象类来说,此标志为真,其他类型为假
ACC_SYNTHETIC0x1000标志此类并非由用户代码生成(即:由编译器产生的类,没有源码对应)
ACC_ANNOTATION0x2000标志这是一个注解
ACC_ENUM0x4000标志这是一个枚举
ACC_MODULE0x8000标志着时一个模块

6. 类索引、父类索引、接口索引集合

在访问标志之后,会指定该类的类别、父类类别以及实现的接口,这三项确定该类的继承关系:

  • 类索引用于确定这个类的全限定名;
  • 父类索引用于确定这个类的全限定名,由于Java语言不允许多继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0;
  • 接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句后的接口顺序从左到右排列在接口索引集合中;

类索引(this_class):2字节无符号整数,指向常量池的索引,它提供了类的全限定名。this_class的值必须是对常量池表中某项的一个有效索引值,常量池在这个索引处的成员必须为CONSTANT_Class_info类型结构体,该结构体表示这个字节码文件所定义的类或接口。

父类索引(super_class):2字节无符号整数,指向常量池的索引,由于Java不支持多继承,所以其父类只有一个。super_class指向的父类不能是final的。

接口索引集合(interfaces):指向常量池索引集合,它提供了一个符号引用到所有已实现的接口。由于一个类可以实现多个接口,因此需要以数组形式保存多个接口的索引。表示接口的每个索引也是一个指向常量池的 CONSTANT_Class (当然这里也就必须是接口,而不是类)。

  • interfaces_count (接口计数器):interfaces_count 项的值表示当前类或接口的直接超接口数量。
  • interfaces [] (接口索引集合):interfaces [] 中每个成员的值必须是对常量池表中某项的有效索引值,它的长度为 interfaces_count。每个成员 interfaces[i] 必须为 CONSTANT_Class_info 结构,其中 0 <= i < interfaces_count。在 interfaces[] 中,各成员所表示的接口顺序和源代码中给定的接口顺序(从左至右)一样,即 interfaces[0] 对应的是源代码中最左边的接口。

7. 字段表

紧接着的是字段表,字段表用于描述接口或类中声明的变量,字段(field)包括类变量和实例变量,但是不包括方法内部、代码块内部声明的局部变量。首先是2个字节的字段表长度,然后是字段表。

注意事项:

  1. 字段表集合中不会列出从父类或者实现的接口中继承而来的字段,但有可能列出原来Java代码中不存在的字段,例如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段;
  2. 在Java语言中,字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来说,如果两个字段的描述符不一致,那么字段重名就是合法的;

字段表条目结构如下:

field_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}
  • access_flags:访问标志

    标志名称解释
    ACC_PUBLIC0x0001字段是 public 的
    ACC_PRIVATE0x0002字段是 private 的
    ACC_PROTECTED0x0004字段是 protected 的
    ACC_STATIC0x0008字段是 static 的
    ACC_FINAL0x0010字段是 final 的
    ACC_VOLATILE0x0040字段是 volatile 的
    ACC_TRANSIENT0x0080字段是 transient 的
    ACC_SYNTHETIC0x1000字段是编译器生成的,不是由源代码直接声明的
    ACC_ENUM0x4000字段是枚举类的元素
  • name_index:名称索引,指向常量池的索引,该索引处的常量必须是 CONSTANT_Utf8_info 类型,表示字段的简单名称(即字段名字符串)。

  • descriptor_index:描述符索引,指向常量池的索引。该索引处的常量必须是 CONSTANT_Utf8_info 类型,表示字段的类型描述符。例如,I表示int[d表示double[]等;

  • attributes_count:属性计数器,表示该字段关联的附加属性的数量;

  • attributes:属性集合,包含了与该字段相关的附加信息。属性的种类和数量由 attributes_count 指定。并不是所有的字段都有属性。常见的字段属性包括

    • ConstantValue: 对于被声明为 static final 且其值是编译时常量的字段,该属性存储了这个常量的值。
    • Signature: 如果字段的类型使用了泛型(如 List<String>),这个属性会存储其泛型签名信息。
    • Deprecated: 表示该字段已被标记为废弃(使用 @Deprecated 注解)。
    • Synthetic: 与 access_flags 中的 ACC_SYNTHETIC 标志对应,表示字段是编译器生成的。
    • 运行时可见/不可见注解 (RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations):存储了应用于该字段的注解信息。

8. 方法表

和字段表类似,方法表用于描述类或接口中的方法。方法表中每项的结构如下:

method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}
  • access_flags:访问标志,表示方法的访问权限和特性,是一个标志位集合(Bitmask)。常见的标志位如下:

    标志名称标志位解释
    ACC_PUBLIC0x0001方法是 public 的
    ACC_PRIVATE0x0002方法是 private 的
    ACC_PROTECTED0x0004方法是 protected 的
    ACC_STATIC0x0008方法是 static 的
    ACC_FINAL0x0010方法是 final 的
    ACC_SYNCHRONIZED0x0020方法是 synchronized 的
    ACC_BRIDGE0x0040方法是编译器生成的桥接方法(用于泛型和类型擦除)
    ACC_VARARGS0x0080方法接受可变数量的参数(varargs)
    ACC_NATIVE0x0100方法是 native 的(用其他语言实现)
    ACC_ABSTRACT0x0400方法是 abstract 的
    ACC_STRICT0x0800方法使用了 Strictfp 浮点运算模式
    ACC_SYNTHETIC0x1000方法是编译器生成的,不是由源代码直接声明的
  • name_index:名称索引,指向常量池的索引。该索引处的常量必须是 CONSTANT_Utf8_info 类型,表示方法的简单名称。对于实例初始化方法(构造器),名称是 <init>;对于类或接口的初始化方法,名称是 <clinit>;其他方法的名称就是源代码中定义的名称。

  • descriptor_index:描述索引,

  • 指向常量池的索引。该索引处的常量必须是 CONSTANT_Utf8_info 类型,表示方法的类型描述符。方法描述符使用一种特殊的字符串格式表示方法的参数列表和返回值类型。格式是 (参数类型描述符列表)返回值类型描述符

    • 括号内的参数类型描述符列表按照参数在方法签名中出现的顺序排列。
    • 如果方法没有参数,参数列表就是 ()
    • 如果方法返回 void,返回值类型描述符就是 V

    例如:

    • ()V: 表示一个没有参数,返回 void 的方法 (void myMethod())。
    • (Ljava/lang/String;I)Z: 表示一个接受 String 和 int 类型参数,返回 boolean 的方法 (boolean myMethod(String s, int i))。
    • ([D)D: 表示一个接受 double 数组,返回 double 的方法 (double myMethod(double[] arr))。
  • attributes_count :属性计数器,表示该方法关联的附加属性的数量。

  • attributes:属性集合,包含了与该方法相关的附加信息。属性的种类和数量由 attributes_count 指定。方法的属性通常比字段的属性更复杂。常见的字段属性包括:

    • Code: 这是最重要也最常见的属性。对于非 abstract 和非 native 的方法,该属性包含了方法的实际字节码指令、操作数栈的最大深度、局部变量表的大小,以及异常处理表等信息。Abstract 或 native 方法没有 Code 属性。
    • Exceptions: 列出了方法可能抛出的异常(即 throws 子句中声明的异常)。
    • Signature: 如果方法的签名使用了泛型(如方法参数或返回值涉及泛型类型),该属性会存储其泛型签名信息。
    • Deprecated: 表示该方法已被标记为废弃。
    • Synthetic: 与 access_flags 中的 ACC_SYNTHETIC 标志对应,表示方法是编译器生成的。
    • MethodParameters: (Java 8 引入) 存储了方法的参数信息,包括参数名和参数的访问标志(如 final)。

9. 属性表

在方法表集合之后的属性表集合,指的是字节码文件所携带的辅助信息,比如该字节码文件的源文件名称,以及任何带有RetentionPolicy.CLASS或者RetentionPolicy.RUNTIME的注解。这类信息通常被用于Java虚拟机的验证和运行,以及Java程序的调试,一般无须深入了解。

.class 文件格式中,我们可以在以下几个地方看到属性表:

  1. ClassFile 结构本身 (Class 文件属性集合)
  2. field_info 结构 (字段属性集合)
  3. method_info 结构 (方法属性集合)
  4. Code 属性 (代码属性中的属性集合,因为 Code 本身也是一个属性,它内部还可以包含属性)

属性的基本结构如下:

attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}
  • attribute_name_index:属性名索引,一个指向常量池的索引。该索引处的常量必须是 CONSTANT_Utf8_info 类型,表示属性的名称字符串(例如:"Code", "ConstantValue", "Exceptions", "SourceFile" 等)。通过这个名称,JVM 或字节码工具可以识别属性的类型和作用。
  • attribute_length:属性长度。
  • info:属性信息,属性的实际内容。这个字节数组的长度由 attribute_length 指定。info 部分的内部结构和内容完全取决于 attribute_name_index 所指向的属性名称。

接下来介绍几个常见的属性:

SourceFile: 存储源文件的名称,其结构如下:

SourceFile_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 sourcefile_index;
}

attribute_name_index:属性名称,指向常量池中的SourceFile字符串;

attribute_length:属性长度,值必须为2;

sourcefile_index:源文件名称,是一个指向常量池中的索引,常量池中该索引处存储一个表示源文件名称字符串。

LocalVariableTable:是在Code属性中的属性,存储局部变量的名称、类型、作用域(字节码范围)等信息(用于调试),结构如下:

LocalVariableTable_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 local_variable_table_length;
    {   u2 start_pc;
        u2 length;
        u2 name_index;
        u2 descriptor_index;
        u2 index;
    } local_variable_table[local_variable_table_length];
}

attribute_name_index:属性名称,即"LocalVariableTable"字符串;

attribute_length:属性长度;

local_variable_table_length:局部变量表长度;

local_variable_table:局部变量表,其内部元素为:

txt
{   
    u2 start_pc;        // 局部变量起始行
    u2 length;          // 局部变量长度(存活的周期)
    u2 name_index;      // 局部变量名称索引
    u2 descriptor_index;// 局部变量描述符
    u2 index;           // 局部变量索引
}

其余属性不再赘述,完整属性见JVM规范:https://docs.oracle.com/javase/specs/jvms/se24/html/jvms-4.html#jvms-4.7

10. javac与javap的使用

10.1 javac

javac (Java 编译器) 命令中,-g 参数用于控制在编译生成的 .class 字节码文件中包含哪些调试信息(debugging information)。通常包括:

  • 源文件信息 (Source file information):.class 文件中包含 SourceFile 属性,记录了编译该 .class 文件对应的源文件名称。这使得调试器能够知道当前执行的代码来自于哪个源文件。
  • 行号信息 (Line number information): 在编译后的方法字节码中(Code 属性内部),包含 LineNumberTable 属性,它记录了字节码指令地址与源代码行号之间的映射关系。这使得调试器可以在源代码级别单步执行和设置断点,错误堆栈跟踪(Stack Trace)中也能显示准确的行号。
  • 局部变量信息 (Local variable information): 在方法的 Code 属性内部,包含 LocalVariableTableLocalVariableTypeTable 属性,记录了方法中局部变量的名称、类型、作用域以及在栈帧中的位置。这使得调试器能够在程序暂停时查看局部变量的名称和值。

10.2 javap

javap 是 JDK 自带的一个命令行工具,用于反汇编(disassemble) Java 字节码文件(.class 文件)。它可以读取 .class 文件的结构信息,并将字节码指令转换为人类可读的格式。

基本用法如下:

bash
javap <options> <classes>

常用的参数有两个:

  • -v-verbose:详细输出,包括
    • .class 文件版本信息。
    • 常量池的完整内容。
    • 类的访问标志 (access_flags)。
    • 字段和方法的完整信息(包括访问标志、名称、描述符)。
    • 每个方法的详细信息,包括 Code 属性的内容,如操作数栈最大深度、局部变量表大小、异常处理表等。
  • -p:显示所有成员,默认情况下,javap 只显示公共和受保护的成员。使用 -p 会显示类的所有成员,包括私有(private)成员和包私有(package-private)成员。

总之,使用javap -p -v xxx输出完整信息。