JVM 系列 —— class 文件格式

这是 JVM 系列的第二篇,见 JVM 系列

在前面的 jvm 基础中,了解了 jvm 的组成等一些基础知识,在 java 程序执行的流程中, .java 文件编译成 .class 文件,那么这个 class 文件到底长什么样子,这篇文章主要就是来介绍一个 class 文件的是什么样的格式。主要参考的是 《java 虚拟机规范 JavaSE 8版》这本书。为了方便看到 class 文件,可以在 IDEA 上装一个插件 jclasslib,格式化了 class 文件,当然也可以用命令 javap -v xxx.class 来查看。还可以下载 Java Byte Editor工具来查看,甚至修改字节码。

总览

下面这张图是一个正确的 class 文件应该有的格式,以及里面的个项的含义:
jdk 1.8 class 文件格式

首先提两点

  1. class 加载后到哪里去了?
    方法区。
    我们知道 java 在类加载之后,会有一个jvm 运行时的内存区域,主要包括 jvm 栈,native 栈,PC,方法区,堆。 方法区实际上就是保存类编译之后的文本代码段,类似于操作系统进程中的 text segment 文本段,存储的是每一个类的结构信息, 里面保存了运行时常量池、类的元数据、字段、构造方法、普通方法等信息。
    方法区是堆的一个组成部分,只是一个逻辑概念,一般来说,不对方法区进行一个垃圾回收。
  2. 运行时常量池是个什么东东?
    是 class 文件中每一个类或者接口的常量池表(constant_pool table)的运行时的表示形式。类似于计算机系统中的符号表(symbol table)。

class 文件格式

每一个 class 文件都对应这唯一一个类或接口的信息,但是并不是都是以磁盘文件 .class 的形式存在,也可以通过类加载器直接生成,一个有效的类或接口所满足的格式统称为“class 文件格式”。

每个 class 都是有字节流组成。这些字节流或以1个字节、2个字节、4个字节等组合而成,中间没有空格或对齐,并且多字节的数据项是以big-edian(大端在前,高位字节在地址的最低位,低位字节在地址的最高位)的顺序进行存储。用 u1,u2,u4 专用的数据类型来表示 class文件的内容,分别表明 1,2,4个字节的无符号数。

class 文件格式各个结构体的内容成为 项(items),比如:u4 magic;,如果由长度不定的项组成的结构称为表(table),用于表示 class 文件内容中的复杂的结构,并且以数组的形式存在。比如:cp_info constant_pool

ClassFile 结构

每一个 class 文件都对应一个 ClassFile 的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
 ClassFile {
// 魔数,固定值 0xCAFEBABE,是不是 class 文件的检验标志
u4 magic;
// 副版本号,比如 0
u2 minor_version;
/*
主版本号,比如 jdk 11 的主版本号是:55
和次版本号配合使用,判断这个 class 文件是否被此虚拟机支持,比如 jdk1.8 默认 52.0,jdk11 默认 55.0
*/
u2 major_version;
// 常量池计数器,值是常量池表的大小+1
u2 constant_pool_count;
/*
常量池表,
常量池索引 0 是无效的,所以常量池表的数组索引范围是: 1 ~ constant_pool_count-1
但是其他使用到常量池的数据结构可以使用索引 0 来表明:“不引用任何一个常量池”

包含 class文件结构机器子结构中引用的所有字符串常量、类或接口、字段名和其他常量
并且每一个项的第一个字节作为类型标记,用于确定该项的格式,这个字节称为标记字节(tag byte)
*/
cp_info constant_pool[constant_pool_count-1]; /*表结构*/
// 访问标志,表明类或接口的访问权限以及属性
u2 access_flags;
/*
类索引,值是对常量池中的某个项的一个有效索引值
常量池在这个索引处的成员必须为 CONSTANT_Class_info 类型结构体,该结构体表示这个 class 文件所定义的类或接口
*/
u2 this_class;
/*
父类索引,值要么是 0,要么是常量池中的某个项的一个有效索引值,如果是 0 ,那么这个类就是 Object 类,唯一没有父类的类。
如果不是 0,那么常量池在这个索引出的成员必须为 CONSTANT_Class_info 类型结构体,表示这个 class 文件定义的类的直接父类,如果这个 class 是接口的话,那么 CONSTANT_Class_info 代表的就是 Object 的结构。
*/
u2 super_class;
// 接口计数器,当前类或接口的直接父接口的数量
u2 interfaces_count;
/*
接口表,数组,里面每一个成员值都是常量池中的某项的有效索引值,长度为 `interfaces_count`
每个成员的结构都是 CONSTANT_Class_info 类型,数组成员的顺序和源代码里面的接口顺序一致
*/
u2 interfaces[interfaces_count];
// 字段计数器,当前 class 文件的 fields 表的成员个数
u2 fields_count;
// 字段表,fields表描述的是当前类的所有字段信息,不包括父类和父类继承的那些字段,其中的每一个成员的类型都是 field_info 结构数据项,表示该类或接口所声明的类字段或者实例字段(是否是 static 修饰)
field_info fields[fields_count];
// 方法计数器,下面 methods 表的成员个数
u2 methods_count;
/*
方法表, methods 表描述的是当前类的所有方法信息,包括实例方法、类方法、构造方法,实例初始化方法或类和接口初始化方法,不包括父类和父类继承的方法
其中的每一个成员的类型都是 method_info
*/
method_info methods[methods_count];
// 属性计数器,当前 class 文件属性表的成员个数
u2 attributes_count;
// 属性表,每一个项的值必须是 attribute_info 结构,常见的泛型、注解都属于 attribute_info
attribute_info attributes[attributes_count];
}

补充说明
  • access_flags (访问标志)
    每个标志的取值和含义如下:
标志名 含义
ACC_PUBLIC 0x0001 声明为 public,可以从包外访问
ACC_FINAL 0x0010 声明为 final,无法继承
ACC_SUPER 0x0020 当用到 invokespecial 指令的时候,需要对父类方法做特殊处理,invokespecial在 JDK1.0.2 发生了改变,为了避免二义性,之后编译的 class 都带该标志
ACC_INTERFACE 0x0200 该 class 文件定义的是接口而不是类
ACC_ABSTRACT 0x0400 声明为抽象类,不能实例化
ACC_SYNTHETIC 0x1000 声明为 synthetic,java 代码中并没有该修饰类型,是由编译器生成的,与 private 修饰符有关系
ACC_ANNOTATION 0x2000 标识注解类型
ACC_ENUM 0x4000 标识枚举类型
ACC_MODULE 0x8000 jdk8 没有,从 jdk9 开始,该类是一个 module,不是类也不是接口,会特殊处理

一个 class 的 access_flags 由这些标志互斥或组合而成,具体可以看 jvm 的规范。

类名称的内部表示形式

class 文件结构中出现的类或者接口的名称,都是通过全限定的形式(fully qualified form)来表示, 称为二进制名称,这个名称使用 CONSTANT_Utf8_info 来表示。比如:

类或接口的二进制限定名表示

类或者接口的二进制名称会被 CONSTANT_NameAndType_info 结构所引用,以便构成它们的描述符。

分隔标识符的符号是斜杠 “/”。

这里注意的是全限定名称和非限定名称:

  • 全限定名
    在整个 JVM 中绝对名称,比如: java.lang.Object,类似于绝对路径。
  • 非限定名
    方法名,字段名,局部变量名和形式参数名都是用非限定名来保存的。比如: Object

描述符

字段描述符

字段描述符的格式:FieldType

对于基本类型的字段,class 文件中的描述格式为:B C D F I J S Z 之一,比如类中的字段 int field, calss 文件描述符为:I
对于对象类型的字段,描述格式为: L ClassName;,比如类中字段 String field,class 文件描述为:Ljava/lang/String;
对于数组类型的字段,描述格式为:[ Component Type,比如类中字段 String[] array,class 文件描述符为:[Ljava/lang/String;,如果是三维数组String[][][] f array,那么描述符为 [[[Ljava/lang/String;

字段描述符解释表:

FieldType 中的字符 类型 含义
B byte 有符号的字节型数
C char Unicode 字符码点, UTF-16编码
D double 双精度浮点型
F float 单精度浮点型
I int 整型数
J long 长整数
L ClassName ; reference ClassName 的实例引用
S short 有符号的短整数
Z boolean 布尔值 true 、 false
[ reference 一个一维数组

方法描述符

class 文件中方法的描述符格式为:(方法形参描述符)返回类型
返回类型有两种,无返回值用 V 表示。举例说明:

1
2
public String methodReturn(int i, String d, Thread t) {...}
public void methodVoid(int i, String d, Thread t) {...}

上面两个方法,在 class 文件中的描述符分别为:
(ILjava/lang/String;Ljava/lang/Thread;)Ljava/lang/String;
(ILjava/lang/String;Ljava/lang/Thread;)V

值得一提的是,静态方法和非静态方法的描述符都是一样的,实例方法除了传递参数以外,还需要传递参数 this ,这一点方法描述无法表明,通过 jvm 虚拟机调用实例方法指令来实现的。

常量池

java 虚拟机指令不依赖于类、类实例、接口或者数组的运行时的布局,而是依赖常量池表中的符号信息

class 文件中有个常量池表 cp_info, 它的里面的每个项都是如下的模板:

1
2
3
4
cp_info {   
u1 tag ;
u2 info [ ] ;
}

tag 里面的值决定了 info[ ] 数组里面存放的信息,tag 的说明如下表:

常量类型 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
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

jdk 11 相对于 jdk 8 新增了一些:

常量类型 tag 值
CONSTANT_Dynamic 17
CONSTANT_Module 19
CONSTANT_Package 20

单独拿最常见的项 CONSTANT_Utf8_info 来说明一下,该项用于表示字符常量的值,它的结构如下:

1
2
3
4
5
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}

tag : 由上面的表可知,这里的 tag 项的值为 CONSTANT_Utf8(1)
length :length 项的值指明了 bytes[] 数组的长度
bytes:表示字符串值得 byte 数组

字段

class 文件中的每个字段 field 都是由 field_info定义,结构如下:

1
2
3
4
5
6
7
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}

里面的项的含义跟上面的差不多,不过多的解释。xxx_index 都是对常量池的一个有效的索引值。
attributes_count 表示对当前字段的附加属性的数量,比如加了注解的字段

方法

1
2
3
4
5
6
7
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}

里面的一些含义和字段基本相同。

属性

属性在 ClassFile 结构中以及字段表和方法表中都在使用,它的结构格式如下:

1
2
3
4
5
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}

jdk8 有 23 种属性, jdk11 有 28种属性。这些属性主要分为三类:

  1. 对 jvm 虚拟机解读 class 文件有关键作用的属性;
  2. 对 jdk类库解读 class 文件有关键作用的属性;
  3. 使用的工具类属性;

着重来看一下,我们最常接触到的属性 Code_attribute,位于 method_info 结构的属性表中,包含某个方法、实例初始化方法、类或接口初始化方法的 java 虚拟机指令以及相关信息。 它的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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];
}

这里面最重要就是 code[] 数组,这里面保存的就是实现该方法的 jvm 代码的字节码内容。举例说明:
方法实现字节码

格式检查

综上所述,jvm 在加载类之前会进行格式检查,主要做以下的检查:

  • 魔数
  • 常量池的格式和约束
    • 常量池中的所有引用名称必须符合规范
  • 属性的格式检查
  • class 不能有多余的字节

这里只是格式检查,并不对比如某个在不在类中进行检查,这种字节码检查会在加载之后。两中检查互不相干。

Java 虚拟机代码约束

虚拟机规范规定了代码的两种约束:静态约束和结构化约束。这两种约束都是对 class文件中 method_info 结构里面的 Code_attribute 的约束。java 的普通方法、初始化方法(包括实例、类或接口初始化方法)的代码的虚拟机指令保存在这里。

  • 静态约束
    静态约束主要是对 Code_attribute 里面的 code 数组中的 java 虚拟机指令进行约束。规定了排序和某些指令必须要有操作数。
  • 结构化约束
    这里的约束主要是对 jvm 虚拟机指令之前的关系进行了规定,比如:
    • 所有的指令都只能在虚拟机栈的栈帧的操作数栈和局部变量表中进行操作。再比如如果指令可以操作 int 类型的值,那同样也可以操作 booleanbytecharshort 类型的值。

      java 虚拟机内部都会把 booleanbytecharshort 转成 int 类型。

    • 调用实例方法或实例变量之前,这个实例已经初始化过的。
      还有很多约束,见java虚拟机规范详细说明。

class 文件校验

上面所说的规范和约束都是为了保证能生成一个正确的 class 文件。但是大多数情况下,我们拿 class 文件过来直接跑应用的,并不会拿源码编译,然后再跑,所以拿到的并不是正确的 class 文件,java 虚拟机还是要自己校验,会在 链接 阶段对 class 文件进行校验来判断是否满足上面说的那些约束。

还需要额外做三项检查:

  1. 保证 final 类没有子类。
  2. 保证 final 方法没有被重写。
  3. 除了 Object 类之外的其他类,都有直接超类。

一句话

只有符合规范和约束的 class 文件才能被正确解析和加载。

作者

操先森

发布于

2021-11-18

更新于

2021-11-18

许可协议

评论