深入理解类文件结构

写在前面

我们都知道 JVM 并不能直接运行 Java 源文件,而是开发者通过 JDK 自带的工具命令 javac将 Java 源文件编译成 class 字节码文件,也就是二进制文件,然后供JVM加载并使用。

为了深入学习这一块的内容,先创建类 User

  • User.java
package com.openmind;

/**
 * jishuzhan
 *
 * @author zhoujunwen
 * @date 2019-11-17
 * @time 20:28
 * @desc
 */
public class User {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

类文件结构

Class类文件结构

编译User.java类,我们用 Sublime Text 打开 User.class 字节码文件,可以看到如下十六进制代码如下:

cafe babe 0000 0034 0021 0a00 0500 1c09
0004 001d 0900 0400 1e07 001f 0700 2001
0004 6e61 6d65 0100 124c 6a61 7661 2f6c
616e 672f 5374 7269 6e67 3b01 0003 6167
6501 0001 4901 0006 3c69 6e69 743e 0100
0328 2956 0100 0443 6f64 6501 000f 4c69
6e65 4e75 6d62 6572 5461 626c 6501 0012
4c6f 6361 6c56 6172 6961 626c 6554 6162
6c65 0100 0474 6869 7301 0013 4c63 6f6d
2f6f 7065 6e6d 696e 642f 5573 6572 3b01
0007 6765 744e 616d 6501 0014 2829 4c6a
6176 612f 6c61 6e67 2f53 7472 696e 673b
0100 0773 6574 4e61 6d65 0100 1528 4c6a
6176 612f 6c61 6e67 2f53 7472 696e 673b
2956 0100 104d 6574 686f 6450 6172 616d
6574 6572 7301 0006 6765 7441 6765 0100
0328 2949 0100 0673 6574 4167 6501 0004
2849 2956 0100 0a53 6f75 7263 6546 696c
6501 0009 5573 6572 2e6a 6176 610c 000a
000b 0c00 0600 070c 0008 0009 0100 1163
6f6d 2f6f 7065 6e6d 696e 642f 5573 6572
0100 106a 6176 612f 6c61 6e67 2f4f 626a
6563 7400 2100 0400 0500 0000 0200 0200
0600 0700 0000 0200 0800 0900 0000 0500
0100 0a00 0b00 0100 0c00 0000 2f00 0100
0100 0000 052a b700 01b1 0000 0002 000d
0000 0006 0001 0000 000b 000e 0000 000c
0001 0000 0005 000f 0010 0000 0001 0011
0012 0001 000c 0000 002f 0001 0001 0000
0005 2ab4 0002 b000 0000 0200 0d00 0000
0600 0100 0000 1000 0e00 0000 0c00 0100
0000 0500 0f00 1000 0000 0100 1300 1400
0200 0c00 0000 3e00 0200 0200 0000 062a
2bb5 0002 b100 0000 0200 0d00 0000 0a00
0200 0000 1400 0500 1500 0e00 0000 1600
0200 0000 0600 0f00 1000 0000 0000 0600
0600 0700 0100 1500 0000 0501 0006 0000
0001 0016 0017 0001 000c 0000 002f 0001
0001 0000 0005 2ab4 0003 ac00 0000 0200
0d00 0000 0600 0100 0000 1800 0e00 0000
0c00 0100 0000 0500 0f00 1000 0000 0100
1800 1900 0200 0c00 0000 3e00 0200 0200
0000 062a 1bb5 0003 b100 0000 0200 0d00
0000 0a00 0200 0000 1c00 0500 1d00 0e00
0000 1600 0200 0000 0600 0f00 1000 0000
0000 0600 0800 0900 0100 1500 0000 0501
0008 0000 0001 001a 0000 0002 001b 
  • class文件是一组以 8 字节为基础的二进制流,用 u1,u2,u4,u8分别表示 1 个字节,2 个字节,4 个字节,8 个字节的无符号数,采用 Big-edian 形式,即高位字节在前
  • 各个数据项严格按照顺序紧凑排列在class文件中
  • class文件中没有任何分隔符,这使得class文件存储的几乎都是可执行代码(在class文件中注释信息已经不复存在)

一个class文件完整地描述了Java源文件的各种信息,Oracle JVM规范中的4.1 The ClassFile Structure 详细定义了一个标准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];
}

我们需要特别注意,1 个字节是2个16进制位,也就是说上面 cafe babe 是 4 个字节,这 4 个字节称之为“魔数”。

1 个字节是 8 位二进制位,表示的范围是 xxxxxxxx,也就是从 00000000-11111111,表示 0 到 255。1 位 16 进制数(用二进制表示为 xxxx)最多只能表示到 15 (即对应的十六进制 F),要表示到 255 就需要两个十六进制位。所以,1个字节=2个16进制字符,一个16进制位=0.5个字节

用图表表示 ClassFile 的数据结构:

class文件结构字节数

魔数

每个 class 文件的前四个字节称为魔数,它的唯一作用就是鉴定是否为一个合法的 class 文件。很多文件存储标准中都使用了魔数来进行身份识别的,譬如 WAV 语音文件的魔数也用 4 个字节表示为: 0x52494646,转为字符串为RIFF

User.class的 16 进制字节码可以看到,class 的魔数为:0xCAFEBABE,我们可以亲切的称呼为“咖啡宝贝”。

Class文件的版本

class 文件中第 5 到 第 8 个字节表示版本号,其中 5、6表示次版本,7、8表示主版本号。Java 版本号是从 45 开始的,JDK1.1 之后的每个JDK大版本发布时,主版本号都要加 1。我们看一看到主版本号为 0x0034,转为十进制表示为 52,由此可以计算出编译 class 文件的JDK版本为JDK8(52-45=7,7+1=8)。

JDK高本版能向下兼容以前的 class 版本,但不能运行以后的版本,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的 Class 文件,否则会抛出java.lang.UnsupportedClassVersionError

主版本号和次版本号一起决定了类文件格式的版本。 如果一个类文件的主版本号为M,次版本号为m,那么我们将它的类文件格式的版本表示为M.m. 因此,类文件格式版本可以按照字典顺序排列,例如,1.5 <2.0 <2.1。 Java虚拟机实现可以支持版本v的类文件格式,当且仅当v处于某个连续范围Mi.0≤v≤Mj.m. 范围基于实现符合的Java SE平台的版本。 符合给定Java SE平台版本的实现必须支持表4.1-A中为该版本指定的范围,并且不支持其他范围。

Java SE版本 class文件格式版本号范围
1.0.2 45.0 ≤ v ≤ 45.3
1.1 45.0 ≤ v ≤ 45.65535
1.2 45.0 ≤ v ≤ 46.0
1.3 45.0 ≤ v ≤ 47.0
1.4 45.0 ≤ v ≤ 48.0
5.0 45.0 ≤ v ≤ 49.0
6 45.0 ≤ v ≤ 50.0
7 45.0 ≤ v ≤ 51.0
8 45.0 ≤ v ≤ 52.0
9 45.0 ≤ v ≤ 53.0
10 45.0 ≤ v ≤ 54.0
11 45 ≤ v ≤ 55
12 45 ≤ v ≤ 56
13 45 ≤ v ≤ 57

常量池

紧接着版本的则是常量池,常量池是Java内存中很重要的一部分。它是 class 文件结构与其他内存结构关联最多的数据类型,也是占用class文件空间最大的数据项之一。

常量池主要存放两类数据:字面量和符号引用。字面量比较接近Java层面的常量,如文本字符串,声明为final的常量值等。而符号引用则包括:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

符号引用属于编译原理中的知识,Java在用javac编译时,不需要在class文件中保存各个方法、属性、字段的最终内存布局信息,而是在jvm虚拟机加载class文件的时候动态链接的。因此,这些方法、字段、属性的符号不经过运行期转换的话是无法直接得到内存入口地址,也就无法直接被虚拟机直接使用。当运行虚拟机时,需要从常量池获取对应的符号引用,再在类创建时或运行时解析,翻译到具体的内存地址。

常量池首先是用 2 字节来表示常量的个数。根据 class 文件的格式,第 9、第 10 表示常量池的个数,我们看看上面User.class的 16 进制,取第 9 位和第 10 位来计算:0x0021=33,这说明我们有 32 个常量,因为第 0 个常量表示不可用,其范围为[1,33]。

  • 常量池是一系列的数组,它的下标是从1开始的,即有效大小实际为 constant_pool_count - 1,第0项是无效的,因此有些结构可以用第0项表示对常量没有引用。
  • 常量池的设计有效减少class文件的大小,想想那些重复使用的类名,字符串表示现在只需要保留一份,并且引用的地方只需要用 u2 保存它在常量池的引用即可。

因为每个常量都有一种具体的类型来代表不同的含义,光知道常量的个数还没办法解析出具体的常量项来,所以定义每个常量的第一个字节u1表示该常量的类型tag,然后就可以根据该类型常量的存储结构来解析了。

​常量的 tag 有:CONSTANT_Utf8,CONSTANT_Integer,CONSTANT_Float,CONSTANT_Long,CONSTANT_Double,CONSTANT_Class,CONSTANT_StringCONSTANT_FieldrefCONSTANT_MethodrefCONSTANT_InterfaceMethodref,CONSTANT_NameAndType,CONSTANT_MethodHandle,CONSTANT_MethodType,CONSTANT_InvokeDynamic等14种。

类型 标志 描述
CONSTANT_Utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整形字面量
CONSTANT_Double_info 6 双精度浮点型字面量
CONSTANT_Class_info 7 类或接口符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法符号的引用
CONSTANT_NameAndType_info 12 字段或方法的符号引用
CONSTANT_MethodHandle_info 16 表示方法类型
CONSTANT_MethodType_info 15 表示方法句柄
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点|​​

CONSTANT_Utf8_info

CONSTANT_Utf8_info 是常量池中最常见的最基本的常量,用来保存一个 utf8 编码的字符串,如常量字符串,类名,字段名,方法名等的值都是对它的引用(偏移量)。

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 byte[length];
}

tag=1,length 表示字符串字节长度,如 length=16,则表示当前结构剩余 16 个字节是一个 utf8 编码的字符串。

需要注意的是:

  • Java 使用的是可变 utf8 编码: ASCII 字符(\u0001~\u0007F,即 1 ~ 127)用 1 个字节表示,null(\u0000)和 \u0080\u07FF 之间用 2 个字节表示, \u8000\uFFFF 之间用 3 个字节表示。

    如果读到一个字节的最高位是 0,则是一个单字节。

    读到一个字节最高3位是110则是一个双字节字符,紧接着还要再读1个字节。

    读到一个字节最高4位是1110,则是一个三字节字符,紧接着还要再读2个字节。

    关于如何解码可以查看官方文档,在java中,我们只需要使用new String(bytes, StandardCharset.UTF8)即可得到解码字符串

  • length使用了u2(0-65535)来表示,则其表示的字符串最大长度为65535(2^16 -1)。

CONSTANT_Integer_info

CONSTANT_Integer_info 常量池保存整形字面量,tag=3,之后的 4 个字节表示存储的整型值。

CONSTANT_Integer_info {
    u1 tag;
    u4 bytes;
}
  • 整形常量池是大端存储 big-endian,字节高位在前。可以用下面的代码解析:
int value = 0;
byte[] data = new byte[4];
is.read(data);
value = (value | (((int) data[0]) & 0xff )) << Byte.SIZE * 3;
value = (value | (((int) data[1]) & 0xff )) << Byte.SIZE * 2;
value = (value | (((int) data[2]) & 0xff )) << Byte.SIZE;
value = (value | (((int) data[3]) & 0xff ));

当然我们可以使用 DataInputStream::readInt() 方法读取一个 int 值。

  • java 中 short, char, byte, boolean 使用 int 来表示,boolean 数组则用 byte 数组来表示(1 个 byte 表示 1 个 boolean 元素)。

CONSTANT_Float_info

CONSTANT_Float_info {
    u1 tag;
    u4 bytes;
}

该常量池用来保存浮点数数据,tag=4,剩余的 4 字节保存浮点数的具体值,采用 IEEE 754标准定义。可以使用DataInputStream::readFloat()方法读取一个float值。

CONSTANT_Long_info

CONSTANT_Double_info {
    u1 tag;
    u4 high_bytes;
    u4 low_bytes;
}

该常量池存储长整型字面值,tag=5,long 和 double 类型在 class 中用两部分(高 4 位和低 4 位)存储。 可以通过 DataInputStream::readLong() 方法读取一个 float 值。

CONSTANT_Double_info

CONSTANT_Double_info {
    u1 tag;
    u4 high_bytes;
    u4 low_bytes;
}

该常量池存储双精度浮点型字面值,tag=6,存储方式同 CONSTANT_Long_info 一样。可以通过 DataInputStream::readDouble() 方法读取一个 double 值。

CONSTANT_Class_info

CONSTANS_Class_info {
    u1 tag;
    u2 name_index; 
}

该常量池用于存储类或接口的信息,tag=7, 注意不是 field 类型或者 method 参数类型、返回值类型。name_index 是常量池,存储的是 CONSTANT_Utf8_info 的索引。

CONSTANT_String_info

CONSTANT_String_info {
    u1 tag;
    u2 string_index;
}

字符串常量池,tag=8,string_index 是常量池的索引,该索引值肯定是一个 CONSTANT_Utf8_info的偏移量,CONSTANT_Utf8_info中存储着实际的字符串值。

CONSTANT_Fieldref_info

CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

tag=9,表示一个引用field信息,包括静态field和实例field。class_index 是常量池中一个CONSTANT_Class_info类型常量(类/接口)的索引,表示字段 field 归属类。name_and_type_index 是常量池中一个CONSTANT_NameAndType_info类型常量的索引,表示字段的名称和类型。

关于 field 引用解释一下,包括下面的 method,接口 method 引用同理:

  • 以本文开头 User 类的 name 字段为例,name 在多个方法中都有用到,相比保存多份该字段信息来讲,在常量池中保存一份该字段信息,然后在其他用到的地方保存其索引显然更合适。
  • CONSTANT_Fieldref_info 是把在代码中引用的 field(可能是本类的,也可能是外部类的)抽离成常量,请不要 class 中的字段表集合 field_info 混淆。

CONSTANT_Methodref_info

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

tag=10,表示引用一个方法 method 的信息,包括静态方法和实例方法。class_index class_index 是常量池中一个CONSTANT_Class_info类型常量(类/接口)的索引,表示方法 method 归属类。name_and_type_index 是常量池中一个CONSTANT_NameAndType_info类型常量的索引,表示方法的名称、参数和返回类型。

CONSTANT_InterfaceMethodref_info

CONSTANT_InterfaceMethodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

tag=11,表示一个接口 method 信息。class_index 是常量池中一个CONSTANT_Class_info类型常量(接口)的索引,表示方法的所属接口。name_and_type_index 同CONSTANT_Methodref_info

CONSTANT_NameAndType_info

CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index; 
}

CONSTANT_NameAndType_info用于存储 field 和 method,类信息等,其 tag=12。name_index指向一个CONSTANT_Utf8_info,表示字段或方法的非全限定名称。descriptor_index也指向一个CONSTANT_Utf8_info,表示字段或方法的描述信息。

descriptor用一个字符串CONSTANT_Utf8_info保存。分为字段描述符和方法描述符。

  • 字段描述符(FieldType): FieldType 可以是基本数据类型:B(byte)C(Char)D(Double)F(Float)I(int)J(long)S(short)Z(boolean),对象类型: L+全限定名称,数组类型: [+元素类型
int a;           // I
Integer b;       // Ljava/lang/Integer
double c;        // D
double[][] d;    // [[D
Object[] e;      // [Ljava/lang/Object
Object[][] f;    // // [[Ljava/lang/Object
  • 方法描述符(MethodDescriptor):MethodDescriptor表示(参数类型)返回类型
/**
* 描述符:(IDLjava/lang/Thread;)Ljava/lang/Object;
*/
Object m(int i, double d, Thread t) {//...}

CONSTANT_MethodHandle_info

CONSTANT_MethodHandler_info {
    u1 tag;
    u1 reference_kind;
    u2 reference_index;
}

tag = 15,方法句柄,比如获取一个类的静态字段,实例字段,调用一个方法,构造器等都会转化成一个句柄。

  • reference_kind
Kind Description(描述) Interpretation(解释)
1 REF_getField getfield C.f:T
2 REF_getStatic getstatic C.f:T
3 REF_putField putfield C.f:T
4 REF_putStatic putstatic C.f:T
5 REF_invokeVirtual invokevirtual C.m:(A*)T
6 REF_invokeStatic invokestatic C.m:(A*)T
7 REF_invokeSpecial invokespecial C.m:(A*)T
8 REF_newInvokeSpecial new C; dup; invokespecial C.init:(A*)V
9 REF_invokeInterface invokeinterface C.m:(A*)T

f: field, m: method, C: 实例构造器,T: 返回类型,(V 是 void类型),A*: 参数

  • reference_index
    • 对于 kind 为 1,2,3,4, reference_index引用一个CONSTATNT_Feildre_info
    • 对于 kind 为 5,6,7,8,reference_index引用一个CONSTANT_Methodref_info
    • 对于 kind=9,reference_index引用一个CONSTANT_InterfaceMethodref_into

CONSTANT_MethodType_info

CONSTANT_MethodType_info {
    u1 tag;
    u2 descriptor_index;
}

tag=16,描述一个方法类型。descriptor_index是一个CONSTANT_Utf8_info,保存方法的描述符。

CONSTANT_InvokeDynamic_info

CONSTANT_InvokeDynamic_info {
    u1 tag;
    u2 bootstrap_method_attr_index;
    u2 name_and_type_index;
}

tag=18,invokedDynamic 动态调用指令信息。

  • bootstrap_method_attr_index: BootstrapMethods属性中 bootstrap_methods[]数组的索引,每个引导方法引用一个CONSTANT_MethodHandler_info常量。
  • name_and_type_index: 动态调用中方法名称等信息,引用一个CONSTANT_NameAndType_info常量。

访问标志

在 class 文件中,访问标志(access flags)占 2 个字节,总共 16bit,该标志用于描述方法或接口的访问信息。比如该类是 class 还是 interface,是 public 还是其他,是否为 abstract,是否被声明为 final 等。

标志名称 标志值 作用域对象 含义
ACC_PUBLIC 0X0001 class, inner, field, method 是否为public
ACC_PRIVATE 0x0002 inner, field, method 是否为private
ACC_PROTECTED 0x0004 inner, field, method 是否为protected
ACC_STATIC 0x0008 inner, field, method 是否为静态static
ACC_FINAL 0x0010 class, inner, field, method 是否为最终final的
ACC_SUPER 0x0020 class 是否允许使用invokespecial字节码指令,JDK1.2之后编译出来的类这个标志为真
ACC_SYNCHRONIZED 0x0020 method 是否为同步,即synchronized修饰
ACC_VOLATILE 0x0040 field 是否被volatitle修饰
ACC_BRIDGE 0x0040 method 桥方法标志,有该标志的方法上同时有ACC_SYNTHETIC标志
ACC_TRANSIENT 0x0080 field
ACC_VARARGS 0x0080 method
ACC_NATIVE 0x0100 method 是否为native方法
ACC_INTERFACE 0x0200 class, inner 是否为接口
ACC_ABSTRACT 0x0400 class, inner, method 是否为抽象类或抽象方法
ACC_STRICT 0x0800 method strictfp,strict float point,方法使用 FP-strict 浮点格式
ACC_SYNTHETIC 0x1000 class, inner, field, method 标识这个类并非由用户代码生产
ACC_ANNOTATION 0x2000 class, inner 标识这是一个注解
ACC_ENUM 0x4000 class, inner, field 标识这是一个枚举

例如:

  • 0x0011(0000 0000 0001 0001):public + final
  • 0x0201(0000 0010 0000 0001):public + 接口

类、父类与接口索引的集合

类索引(this_class)和父类索引(super_class)都是一个 u2 类型的数据,而接口索引集合(interfaces)是一组 u2 类型的数据的集合,class 文件中由这三项数据来确定这个类的继承关系。类索引(this_class)用于确定这个类的全限定名,父类索引(super_class)用于确定这个类的父类的全限定名。由于 Java 不允许多多继承,所以父类所以只有一个,除了java.lang.Object外,所有 Java 父类的索引都不为 0。接口索引集合就是用来描述该类实现了哪些接口,这些被实现了的接口就按照 implements 语句(如果这个类本身是一个接口,则应当是 extends 语句)后的接口顺序从左到右排列在接口索引的集合中。

  • this class: 当前类或接口,指向一个CONSTANT_Class_info常量,可以解析出当前类的全限定名,其中包名层次用/,而不是.,如 java/lang/Object
  • super class: 当前类的直接父类,指向一个CONSTANT_Class_info常量,当没有直接父类时,super_class = 0。
  • interfaces: 首先用 u2 表明当前类或接口的直接父接口数量 n。紧接着 n 个 u2 组成的数组即是这些父接口在常量池的索引,类型为CONSTANT_Class_info,按声明顺序从左至右。

字段表集合

字段(field)其实就是接口或者类中定义的变量,有实例变量和类变量之分。字段表(field_info)用于描述这些字段。方法中定义的变量不能算作字段,字段特指定义在接口或者类中的变量。

field_info {
    u2                  access_flags;
    u2                  name_index;
    u2                  descriptor_index;
    u2                  attributes_count;
    attribute_info      attributes[attributes_count];
}

在字段表之前有一个 u2 字节的数据表示字段个数。然后紧跟的是field_info,保存当前类的fields信息。

  • access_flags:占用两个字节,它描述了该字段的基本访问标志,主要包括:字段的作用域、实例或类变量(static)、可否序列化(transient)、可变性(final)、并发可见性(volatile修饰符)等,每种状态使用一个比特位来标识对于该状态的修饰与否。

表 字段访问标志

标志名称 标志值 含义
ACC_PUBLIC 0x0001 字段是否是 public
ACC_PRIVATE 0x0002 字段是否是 private
ACC_PROTECTED 0X0004 字段是否是 protected
ACC_STATIC 0x0008 字段是否是 static
ACC_FINAL 0x0010 字段是否是 final
ACC_VOLATILE 0x0040 字段是否是 volatile
ACC_TRANSIENT 0x0080 字段是否是 transient
ACC_SYNTHETIC 0x1000 字段是否是 synthetic
ACC_ENUM 0x4000 字段是否是 enum

  • name_index:占两个字节,它存储的是当前字段名称在常量池中的偏移量,也就是内存地址。
  • descriptor_index:占用两个字节,它是对当前字段数据类型的描述,存储的也是一个字符在常量池中的偏移量。但是如果你对应到常量池查看的话,你会发现这个描述符的值是:I。

表 描述符标识含义

标识字符 含义
B 基本数据类型 byte
C 基本数据类型 char
D 基本数据类型 double
F 基本数据类型 float
I 基本数据类型 int
J 基本数据类型 long
S 基本数据类型 short
Z 基本数据类型 boolean
V 特殊类型 void
L 对象类型,如 Ljava/lang/Object

基本数据类型与实际存储的符号之间有这么一种映射关系,为的是简单存储。其中,如果字段是数组类型的话,需要前置一个 『[ 』,多维数组就前置多个该符号进行描述,如[B表示是一个一维 byte 数组。

  • attributes_count: 属性数量。如果没有属性,则为 0。
  • attribute_info: 属性表,此处的属性表描述的是字段中携带子集的属性表集合,以用于描述某些场景专有的信息。具体的分析放在属性表集中。

方法集合

方法集合(method_info)保存当前类的方法信息。在 method_info 之前紧靠的是 methods_count,methods_count 记录该类或接口中的方法个数。方法集合同 field_info。

method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

属性表集

属性表:属性存在与 ClassFile, field_info, method_info 中,此外 Code 属性中又包含嵌套属性信息,属性用来描述指令码,异常,注解,泛型等信息,JLS8预定义了23种属性,每种属性结构不同(变长),但可以抽象成下面通用结构。

attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}
  • attribute_name_index: 是该属性在常量池中的偏移量(内存地址的引用),通过该名称才可以判定当前属性属于具体的哪一种,如“Code”表示当前是一个Code_attribute属性。
  • attribute_length: 表示接下来多少字节属于该属性的内容信息,Java 允许自定义新的属性,如果 jvm 不认识,则按通用结构直接读取 attribute_length 个字节。
  • info:存放着 23 中属性中的任意一种。23 中属性则按作用可分为 3 组:
    • 被jvm翻译使用:ConstantValue,Code, StackMapTable, Exceptions, BootstrapMethods
    • 被java类库解析使用: InnerClasses, EnclosingMethod, Synthetic, Signature, RuntimeVisibleAnnotations/RuntimeInvisibleAnnotations, RuntimeVisibleParameterAnnotations/RuntimeInvisibleParameterAnnotations, RuntimeVisibleTypeAnnotations/RuntimeInvisibleTypeAnnotations, AnnotationDefault, MethodParameters
    • 既不要求jvm解析,也不要求java类库解析,用于调试工具等场景: SourceFile, SourceDebugExtension, LineNumberTable, LocalVariableTable, LocalVariableTypeTable, Deprecated

ConstantValue

ConstantValue_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 constantvalue_index;
}

存在于field_info,代表一个常量值,如private final int x = 5;中的5。attribute_name_index 引用的值是“ConstantValue”,attribute_length是固定值2,两个字节的 constantvalue_index 是该常量值在常量池中的索引,是Constant_LongConstant_FloatConstant_DoubleConstant_IntegerConstant_String的一种。

Code

Code_attribute {
    u2 attribute_name_index;
    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];
}

描述方法体编译后的字节码指令。前面讲过描述方法的method_info结构,而方法的方法体信息就存在它的属性表中的 code 属性内。如果是抽象方法,则没有这个属性。

前面属性通用架构attribute_info中已经涉及attribute_name_indexattribute_length,它是每个属性都有的,后面不再说明,只对其他部分介绍。

  • max_stack,操作数栈的最大深度,用来分配栈的大小。
  • max_locals,方法栈中局部变量表最大容量,存储局部变量、方法参数、异常参数等。以 slot 为单位,32-bit 以内的变量分配 1 slot,大于 32-bit,如longdouble则分配 2 slot,注意对象存的是引用。另外指出一点,对于实例方法,默认会传入this对象指针,所以这时的max_locals最小为 1。
  • code[code_length],存储字节码指令列表,每条字节码指令是一个 byte,这样,8bit 最多可以表示256条不同的指令,需要指出的是,这个字节流数组存的不全是指令,有的指令还有对应的操作数,跳过相应 n 个字节的操作数后才是下一条指令。
  • exception_table[exception_table_length],方法异常表,注意不是方法申明抛出的异常,而是显示 tyr-catch 的异常,每个 catch 的异常是 exception_table 的一项。
    • catch_type,捕获的异常类型,指向一个 CONSTANT_Class_info 常量。
    • start_pc,字节码指令相对方法开始的偏移量,相当于 code[code_length] 中的索引。
    • end_pc,字节码指令相对方法结束的偏移量,相当于code[code_length] 中的索引。
    • handler_pc,字节码指令相对方法处理的偏移量,相当于code[code_length] 中的索引。

    这几项表示的意思是,如果在[start_pc,end_pc]区间发生了 try-catch 类型或其他子类型的异常(catch_type=0表示补货任意异常),则跳转至 handler_pc 处的指令继续执行。

    补充三点:
    (1) 关于 finaly 中的指令采用方式是在每个代码分支中冗余一份。
    (2) 关于未显示捕获的异常,则通过athrow指令继续抛出。
    (3) 虽然指令长度 code_length 是 u4,但 start_pc, end_pc, handler_pc 都只有 2 个字节的无符号数 u2,最大范围只能表示 65535,因此方法最多只能有65535条指令(每条指令都不带操作数的情况下)。

  • attributes[attributes_count],嵌套属性列表。

StackMapTable

StackMapTable_attribute {
    u2              attribute_name_index;
    u4              attribute_length;
    u2              number_of_entries;
    stack_map_frame entries[number_of_entries];
}

上面讲到的 code_attribute 中也可以包含属性表,StackMapTable 就位于 code 属性的属性表中,它是为了在 jvm 字节码验证阶段做类型推到而添加的。

Exceptions

Exceptions_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 number_of_exceptions;
    u2 exception_index_table[number_of_exceptions];
}

表示通过throws声明可抛出的异常,结构很简单,exception_index_table 的每一项 u2 指向一个 CONSTANT_Class_info 常量。

BootstrapMethods

BootstrapMethods_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 num_bootstrap_methods;
    {   u2 bootstrap_method_ref;
        u2 num_bootstrap_arguments;
        u2 bootstrap_arguments[num_bootstrap_arguments];
    } bootstrap_methods[num_bootstrap_methods];
}

位于 classFile 中,保存invokedynamic指令引用的引导方法。

  • bootstrap_method_ref,引用一个 CONSTANT_MethodHandle_info 常量,此时该 MethodHandle 的 reference_kind 必定为 REF_invokeStatic 或 REF_newInvokeSpecial。
  • bootstrap_arguments,引导方法参数列表,数组中的每一项是一个CONSTANT_String_info,CONSTANT_Class_info,CONSTANT_Long_info,CONSTANT_Double_info,CONSTANT_Float_info,CONSTANT_Integer_info,CONSTANT_MethodHandle_info,CONSTANT_MethodType_info的一种。

InnerClasses

InnerClasses_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 number_of_classes;
    {   u2 inner_class_info_index;
        u2 outer_class_info_index;
        u2 inner_name_index;
        u2 inner_class_access_flags;
    } classes[number_of_classes];
}

记录内部类信息,classes 就是当前内部类列表,其中 innner_class_info_index 和 outer_class_info_index 都指向CONSTANT_Class_info常量,分别代表内部类和外部类信息引用,inner_name_index 是内部类名称的引用,指向CONSTATNT_Utf8_info常量,等于0则代表是匿名内部类,inner_class_access_flags 是内部类访问标志,同 access_flag

EnclosingMethod

EnclosingMethod_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 class_index;
    u2 method_index;
}

位于 classFile 的结构中,存储局部类或匿名类信息。

  • class_index,对直接包含它的类的引用,引用一个 CONSTANT_Class_info 常量,代表包含当前类的最内层类。
  • method_index,引用一个 CONSTANT_NameAndType_info 常量,表示直接包含该局部内、匿名类的方法名称或类型。

Synthetic

Synthetic_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
}

标记类、方法、字段是否编译器生成, 与 ACC_SYNTHETIC 同义。attribute_length=0,存在该属性则表示 true。

Signature

Signature_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 signature_index;
}

存在于类、方法、字段的属性表中,用于存储类、方法、字段的泛型信息(类型变量 Type Variables,参数化类型 Parameterized Types)。

signature_index 引用一个 CONSTANT_Utf8_info 常量,表示签名。

RuntimeVisibleAnnotations

RuntimeVisibleAnnotations_attribute {
    u2         attribute_name_index;
    u4         attribute_length;
    u2         num_annotations;
    annotation annotations[num_annotations];
}

存在于类、方法、字段,存储运行时可见的注解信息(RetentionPolicy.RUNTIME),可以被反射 API 获取到。annotation 结构存储了注解名称、元素值对的信息。

RuntimeInvisibleAnnotations

RuntimeInvisibleAnnotations_attribute {
    u2         attribute_name_index;
    u4         attribute_length;
    u2         num_annotations;
    annotation annotations[num_annotations];
}

与RuntimeVisibleAnnotations结构相同,但不可见,即不能被反射API获取到,目前jvm忽略此属性。

RuntimeVisibleParameterAnnotations

RuntimeVisibleParameterAnnotations_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 num_parameters;
    {   u2         num_annotations;
        annotation annotations[num_annotations];
    } parameter_annotations[num_parameters];
}

存在于method_info的属性表中,存储运行时可见的方法参数注解信息,与RuntimeVisibleAnnotations 对比发现,RuntimeVisibleParameterAnnotations 存储的是方法的参数列表上每个参数的注解(相当与一组 RuntimeVisibleParameterAnnotations),顺序与方法描述符中参数顺序一致。

RuntimeInvisibleParameterAnnotations

RuntimeInvisibleParameterAnnotations_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 num_parameters;
    {   u2         num_annotations;
        annotation annotations[num_annotations];
    } parameter_annotations[num_parameters];
}

RuntimeVisibleTypeAnnotations

RuntimeVisibleTypeAnnotations_attribute {
    u2              attribute_name_index;
    u4              attribute_length;
    u2              num_annotations;
    type_annotation annotations[num_annotations];
}

存在于class_file,method_info,field_info,code 的属性表中,java8 新增。JLS8新增两种 ElementType(ElementType.TYPE_PARAMETER, ElementType.TYPE_USE),相应用来描述的注解属性也做了相应的改的,就有了该属性,type_annotation 存储着注解信息及其作用对象。

RuntimeInvisibleTypeAnnotations

RuntimeInvisibleTypeAnnotations_attribute {
    u2              attribute_name_index;
    u4              attribute_length;
    u2              num_annotations;
    type_annotation annotations[num_annotations];
}

AnnotationDefault

AnnotationDefault_attribute {
    u2            attribute_name_index;
    u4            attribute_length;
    element_value default_value;
}

存在于method_info属性表 ,记录注解元素的默认值。

MethodParameters

MethodParameters_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 parameters_count;
    {   u2 name_index;
        u2 access_flags;
    } parameters[parameters_count];
}

存在于method_info属性表 ,记录方法参数信息,name_index 形参名称,access_flags 有 ACC_FINAL,ACC_SYNTHETIC,ACC_MANDATED 。

SourceFile

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

class_file属性表中,记录生成该的文件名,异常堆栈可能显示此信息,一般与类名相同,但内部类不是。这是一个可选属性,意味着不强制编译器生成此信息。

SourceDebugExtension

SourceDebugExtension_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 debug_extension[attribute_length];
}

存在于class结构中,可选,保存非java语言的扩展调试信息。debug_extension 数组是指向 CONSTAN_Utf8_info 的索引。

LineNumberTable

LineNumberTable_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 line_number_table_length;
    {   u2 start_pc;
        u2 line_number; 
    } line_number_table[line_number_table_length];
}

code 的属性表中,存储源码行号与字节码偏移量(方法第几条指令)之间映射关系,start_pc 字节码偏移量,line_number 源码行号,可选。

LocalVariableTable

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

code的属性表中,存储栈帧中局部变量表的变量与源码中定义的变量的映射,可以在解析code属性时关联到局部变量表变量在源码中的变量名等,可选。

LocalVariableTypeTable

LocalVariableTypeTable_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 local_variable_type_table_length;
    {   u2 start_pc;
        u2 length;
        u2 name_index;
        u2 signature_index;
        u2 index;
    } local_variable_type_table[local_variable_type_table_length];
}

code 的属性表中,与 LocalVariableTable 相似,signature_index 也引用一个CONSTANT_Utf8_info 常量,对应含有泛型的变量会同时存储到 LocalVariableTable 和 LocalVariableTypeTable 中个一份。

Deprecated

Deprecated_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
}

类、方法、字段过期标记,没有额外信息,attribute_length=0,如果出现该属性则说明加了 @deprecated 注解。

JVM的限制

  • classFile结构中u2(16-bit)的constant_pool_count限制了 per-class 或者 per-interface 的常量池的大小,最多只能有 65535 个条目(entries)。这对单个类或者接口的复杂性起到类内部限制作用。
  • classFile结构中fields_count限制了一个类或者接口能声明的field数量不能超过 65535(不包括继承的)。
  • classFile结构中methods_count限制了一个类或者接口能声明的method数量不能超过 65535(不包括继承的)。
  • 直接父接口的数量限制同上(interfaces_count)。
  • 方法调用时创建的帧里面,本地变量表中的本地变量数量最多不能超过 65535,由code属性中的max_locals item 所限制,以及由 JVM 指令集的 16-bit 本地变量索引所限制。其中longdouble类型视为两个本地变量。
  • code属性中的max_stack item 限制了frame中的操作数栈的大小为 65535。其中,longdouble类型视为两个操作单元。
  • method descriptor方法描述符限制了方法参数最多为 255 个,其中实例单元this占用一个,longdouble占用两个单元。
  • field和方法名称、field和方法描述符以及其他String常量值(包括ConstantValue attribute引用)最多为 65535 个 byte,由 CONSTATNT_Utf8_info结构中的 16-bit 无符号length item限制。
  • 数组的维度最多 255,由multianewarray指令中的 opcode dimensions的大小所限制。

参考文章

1.反认他乡是故乡——class字节码,这次我算看透你了!
2.Gordon——Java class文件格式
3.薛勤的博客——深入理解Java虚拟机(类文件结构+类加载机制+字节码执行引擎)
4. 周志明——《深入理解Java虚拟机(第2版)》
5. Java Virtual Machine Specification ——The class File Format

感谢上面三维作者的总结,对我理解 classfile 起到特别大的帮助,文章中部分内容直接来源于上述文章。

点击量:24

发表评论