《深入理解Java虚拟机》 类 & class文件

———— JDK 1.7
words: 4.8k    views:    time: 18min

class文件是一种字节码存储格式,它只与Java虚拟机绑定,因此它是Java实现平台无关性的基础。另外,任何其它语言,只要能编译成一个有效class文件,那么也可以在Java虚拟机上执行,因此,通过class这个媒介,Java虚拟机同时拥有语言无关的中立特性

1. 类文件结构

class文件是一组以8字节为单位的二进制流,各个数据项严格按照顺序紧凑地排列,没有任何分隔符。当遇到需要占用8字节以上空间的数据项时,则会按照高位在前的方式分割成若干8字节进行存储。

它采用一种类似于C语言结构体的伪结构来存储数据,且只有两种数据类型:

  • 无符号数:基本数据类型,以u1,u2,u4,u8来分别代表1字节,2字节,4字节和8字节的无符号数;可用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。

  • 表:是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,其中的数据项按顺序可以如下所示:

名称 类型 数量 描述
magic u4 1 Class文件标志:0XCAFEBABE
minor_version u2 1 小版本号
major_version u2 1 大版本号
constant_pool_count u2 1 常量池的数量
constant_pool cp_info constant_pool_count - 1 常量池
access_flags u2 1 Class的访问标志
this_class u2 1 当前类索引
super_class u2 1 父类索引
interfaces_count u2 1 接口计数
interfaces u2 interfaces_count - 1 接口索引集合
fields_count u2 1 字段计数
fields field_info fields_count 字段表集合
methods_count u2 1 方法计数
methods method_info methods_count 方法表集合
attributes_count u2 1 属性计数
attributes attribute_info attributes_count 属性表集合

1.1. 魔数与版本号

很多文件存储标准都使用魔数来进行身份识别,比如图片格式gif,或者jepg,使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为扩展名可以随意地改动。

class文件的头4个字节即为固定的魔数值:0XCAFEBABE,意为咖啡宝贝。紧接着魔数的4字节则为class文件的版本号。

1.2. 常量池

紧接着版本号的是常量池,入口是一项u2类型的数据,代表常量池的计数(从1开始),其中主要存放两大类常量:

  • 字面量:接近Java语言层面的常量概念,如文本字符串、声明为final的常量值等;
  • 符号引用:属于编译原理的概念,包括三类常量:类和接口的全限定名;字段的名称和描述符;方法的名称和描述符;

java代码在进行javac编译的时候,并不像c或C++那样有连接这一步骤,而是在虚拟机加载class文件的时候进行动态连接。也就是说,class文件中不会保存各个方法、字段的最终内存布局。如果这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存地址,则无法被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址。

常量池本身也是一张表,并且它里面的每一项常量也是一个表,共有14种常量类型,每种类型的结构都不一样,不过它们都有一个共同特点,即第一位u1都是类型标志位tag,具体如下表所示:

常量 项目 类型 描述
CONSTANT_Utf8_info tag u1 1:UTF-8编码字符串
length u2 utf8编码的字符串占用的字符数
bytes u1 字符串编码
CONSTANT_Integer_info tag u1 3:整型字面量
bytes u4 高位在前的int值
CONSTANT_Float_info tag u1 4:浮点型字面量
bytes u4 高位在前的float值
CONSTANT_Long_info tag u1 5:长整型字面量
bytes u8 高位在前的long值
CONSTANT_Double_info tag u1 6:双精度浮点型字面量
bytes u8 高位在前的double值
CONSTANT_Class_info tag u1 7:类或接口的符号引用
index u2 指向全限定名常量项的索引
CONSTANT_String_info tag u1 8:字符串类型字面量
index u2 指向字符串字面量的索引
CONSTANT_Fieldref_info tag u1 9:字段的符号引用
index u2 指向声明字段的类或接口描述符CONSTANT_Class_info的索引项
index u2 指向字段描述符CONSTANT_NameAndType_info的索引项
CONSTANT_Methodref_info tag u1 10:类中方法的符号引用
index u2 指向声明方法的类描述符CONSTANT_Class_info的索引项
index u2 指向名称及类型描述符CONSTANT_NameAndType_info的索引项
CONSTANT_InterfaceMethodref_info tag u1 11:接口中方法的符号引用
index u2 指向声明方法的接口描述符CONSTANT_Class_info的索引项
index u2 指向名称及类型描述符CONSTANT_NameAndType_info的索引项
CONSTANT_NameAndType_info tag u1 12:字段或方法的部分符号引用
index u2 指向该字段或方法名称常量项的索引
index u2 指向该字段或方法描述符常量项的索引
CONSTANT_MethodHandle_info tag u1 15:标识方法句柄
reference_kind u1 取值1~9,它决定了方法句柄的类型,即方法句柄的字节码行为
reference_index u2 值必须是对常量池的有效索引
CONSTANT_MethodType_info tag u1 16:标识方法类型
descriptor_index u2 指向CONSTANT_Utf8_info的索引,表示方法描述符
CONSTANT_InvokeDtnamic_info tag u1 18:表示一个动态方法调用点
bootstrap_method_attr_index u2 对当前Class文件中引导方法表bootstrap_methods[]的索引
name_and_type_index u2 指向CONSTANT_NameAndType_info的索引,表示方法名和方法描述符

1.3. 访问标志

在常量池之后,紧接着的两个字节代表访问标志access_flags,用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口、是否为public类型、是否为abstract类型、类是否声明为final等。

access_flags中一共有16个标志位可以使用,当前只定义了其中8个,没用到的标志位要求一律为0

标志名称 标志值 含义
ACC_PUBLIC 0X0001 是否为public类型
ACC_FINAL 0X0010 是否被声明为final,只有类可以设置
ACC_SUPER 0X0020 是否允许使用invokespecial字节码指令的新语意,因为invokespecial指令的语意在JDK1.0.2发生过改变
ACC_INTERFACE 0X0200 标志这是一个接口
ACC_ABSTRACT 0X0400 是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类为假
ACC_SYNTHETIC 0X1000 标志这个类并非由用户代码产生的
ACC_ANNOTATION 0X2000 标志这是一个注解
ACC_ENUM 0X4000 标志这是一个枚举

1.4. 类/父类/接口索引集合

类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info常量中的索引值找到定义在CONSTANT_Utf8_info中的全限定名称字符串

接口索引集合的入口第一项是u2类型的数据,表示接口索引计数interfaces_count。如果该类没用实现任何接口,则值为0,后面的接口索引表也不再占用任何字节

1.5. 字段表集合

字段表用于描述接口或类中声明的变量,包括类级变量以及实例级变量,但是不会列出超类或者从父类继承而来的字段。不过,有可能列出代码中不存在的字段,比如在内部类中为了保持对外部类的访问,会自动添加指向外部类实例的字段。其结构如下:

名称 类型 数量 描述
access_flags u2 1 修饰标志
name_index u2 1 字段名称,对常量池的引用
descriptor_index u2 1 字段描述符,对常量池的引用
attribute_count u2 1 字段属性计数
attributes attribute_info attribute_count 字段属性表

其中对于access_flags与类的access_flags类似,都是一个u2类型数据,其中的标志位含义如下:

标志名称 标志值 含义
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 0X0100 字段是否由编译器自动产生的
ACC_ENUM 0X0400 字段是否enum
1.5.1. 描述符
  • 简单名称:没有类型和参数的方法或者字段名称,比如inc()方法和m字段的简单名称分别为incm
  • 全限定名:将类全名中的.替换成/,并在最后添加一个;表示全限定名结束;
  • 描述符:相对复杂一些,用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值;

描述符规则:

标识字符 含 义 标识字符 含 义
B byte J long
C char S short
D double Z boolean
F float V void
I int L 对象类型,比如Ljava/lang/Object;
  • 基本数据类型以及void都用一个大写字符来表示
  • 对象类型用字符L加对象的全限定名来表示
  • 数组类型,每一纬度使用一个前置的[字符来描述,比如java.lang.String[][]会被描述成[[Ljava/lang/Stringint[]会被描述成[I
  • 描述方法时,参数列表在前,返回值在后,且参数列表需要按顺序放在一组小括号之内,比如java.lang.String的方法toString()会被描述成()Ljava/lang/String,方法int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount,int fromIndex)会被描述成([CII[CII)I

1.6. 方法表集合

Class文件中对方法的描述与对字段的描述几乎一致,只是在访问标志和属性表集合的可选项中有所区别。相应的,如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息,但有可能出现由编译器自动添加的方法,如类构造器方法和实例构造器方法

名称 类型 数量 描述
access_flags u2 1 修饰标志
name_index u2 1 方法名称,对常量池的引用
descriptor_index u2 1 方法描述符,对常量池的引用
attribute_count u2 1 方法属性计数
attributes attribute_info attribute_count 方法属性表

其中关于access_flags中各个标志位的含义:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 方法是否为public
ACC_PRIVATE 0X0002 方法是否为private
ACC_PROTECTED 0X0004 方法是否为protected
ACC_STATIC 0X0008 方法是否为static
ACC_FINAL 0X0010 方法是否为final
ACC_SYNCHRONIZED 0x0020 方法是否为synchronized
ACC_BRIDGE 0x0040 方法是否由编译器产生的桥接方法
ACC_VARARGS 0x0080 方法是否接受不定参数
ACC_NATIVE 0x0100 方法是否为native
ACC_ABSTRACT 0x0400 方法是否为abstract
ACC_STRICTFP 0x0800 方法是否为strictfp
ACC_SYNTHETIC 0x1000 方法是否是由编译器自动产生的

1.7. 属性表集合

Class文件、字段表、方法表都可以有自己的属性表集合,用于描述某些场景的专有信息。属性表集合的限制宽松一些,不要求各个属性表具有严格顺序,并且只要不与已有属性名重复即可,任何人实现的编译器都可以向属性表中写入自定义的属性信息,Java虚拟机在运行时会忽略掉它不认识的属性。

在《Java虚拟机规范(Java SE 7)》中,预定义了21种虚拟机应该能识别的属性:

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 一个定长属性,用来通知虚拟机自动为静态变量赋值
Deprecated 类/方法表/字段表 被声明为deprecated的方法和字段
Exceptions 方法表 列举方法中可能抛出的受检查异常
EnclosingMethod 类文件 仅当一个类为局部类或者匿名类时才能拥有这个属性,用于标识这个类所在的外围方法
InnerClasses 类文件 内部类列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系
StackMapTable Code属性 JDK1.6新增,供新的类型检查器检查目标方法的局部变量和操作数栈所需要的类型是否匹配
Signature 类/方法表/字段表 JDK1.5新增,用于支持泛型情况下的方法签名,为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息
SourceFile 类文件 记录源文件名称
SourceDebugExtension 类文件 DK1.6新增,用于存储额外的调试信息。比如JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号,JSR-45规范为这些非Java语言编写,却需要编译成字节码并运行在Java虚拟机中的程序提供了一个进行调试的标准机制,此属性就用于存储这个标准所新加入的调试信息
Synthetic 类/方法表/字段表 标识方法或字段为编译器自动生成的
LocalvariableTypeTable JDK1.5新增,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations 类/方法表/字段表 JDK1.5新增,为动态注解提供支持。用于指明哪些注解是运行时可见的(即反射调用)
RuntimeInvisibleAnnotations 类/方法表/字段表 JDK1.5新增,作用与RuntimeVisibleAnnotations刚好相反,用于指明那些注解是运行时不可见的
RuntimeVisibeParameterAnnotations 方法表 JDK1.5新增,作用与RuntimeVisibleAnnotations类似,只不过作用对象为方法参数
RuntimeInvisibeParameterAnnotations 方法表 JDK1.5新增,作用与RuntimeInvisibleAnnotations类似,只不过作用对象为方法参数
AnnotationDefault 方法表 JDK1.5新增,用于记录注解类元素的默认值
BootstrapMethods 类文件 JDK1.7新增,用于保存invokedynamic指令引用的引导方法限定符
1.7.1. Code属性表

Code是最重要的一个属性,如果把一个Java程序中的信息分为代码(方法体中的代码)和元数据(包括类、字段、方法定义以及其他信息),那么整个class文件中,Code用于描述代码,所有其他数据项都用于描述元数据

java程序方法体中的代码经过javac编译处理后,最终变为字节码指令存储在Code属性内。Code属性作用在方法表的属性集合中,但并不是所有方法都必须拥有这个属性,比如接口或抽象类中的方法。

如果存在Code属性,那么它的结构如下:

名称 类型 数量 描述
attribute_name_index u2 1 指向常量池中一个CONSTANT_Utf8_info类型的常量,来表示属性名称
attribute_length u4 1 属性值长度
max_stack u2 1 表示操作栈深度的最大值
max_locals u2 1 表示局部变量表所需的存储空间
code_length u4 1 表示代码字节码长度,理论上最大可以达到2^32-1,但虚拟机规定一个方法不能超过65535条字节码指令,否则Javac编译器会拒绝编译
code u1 code_length 用来存储字节码指令的一系列字节流
exception_table_length u2 1 异常表长度
exception_table exception_info exception_table_length 用来实现Java异常及finally处理机制
attributes_count u2 1 Code属性总数
attributes attribute_info attributes_count Code属性

这里说一下Code属性中的异常表,异常表是java代码的一部分,编译器使用异常表而不是简单的跳转指令来实现java异常及finally的处理

Test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test {

public static final int NUM = 1;

private String name;

public String test() throws IOException {
String x;
try{
x = "a";
return x;
}catch(IllegalArgumentException e){
x = "b";
return x;
}finally{
x = "c";
}
}

}

对于上面的代码可以知道:

  • 如果try中没有异常,那么返回a;
  • 如果try中出现IllegalArgumentException或其子类异常,那么返回b;
  • 如果try中出现非IllegalArgumentException异常,那么方法异常退出;

下面通过其字节码看一下具体是如何实现的,关于Java字节码指令,可以参考:https://shanhm1991.github.io/2018/10/05/20181005/

javap -verbose Test.class
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
  public java.lang.String test() throws java.io.IOException;
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Exceptions:
throws java.io.IOException
Code:
stack=1, locals=5, args_size=1
0: ldc #25 // 将字符串常量"a"的引用压入栈顶
2: astore_1 // 将栈顶引用存入局部变量2,即 x = "a"

3: aload_1 // 将局部变量2压入栈顶
4: astore 4 // 将栈顶引用存入局部变量5(返回值),即将返回值赋为"a"

6: ldc #27 // 将字符串常量"c"的引用压入栈顶
8: astore_1 // 将栈顶引用存入局部变量2,即 x = "c"

9: aload 4 // 将局部变量5压入栈顶
11: areturn // 返回栈顶,即 return "a"

12: astore_2 // 将栈顶的引用(catch的IllegalArgumentException)存入局部变量3

13: ldc #29 // 将字符串常量"b"的引用压入栈顶
15: astore_1 // 将栈顶引用存入局部变量2,即 x = "b"

16: aload_1 // 将局部变量2压入栈顶
17: astore 4 // 将栈顶引用存入局部变量5(返回值),即将返回值赋为"b"

19: ldc #27 // 将字符串常量"c"的引用压入栈顶
21: astore_1 // 将栈顶引用存入局部变量2,即 x = "c"

22: aload 4 // 将局部变量5压入栈顶
24: areturn // 返回栈顶,即 return "b"

25: astore_3 // 将栈顶的引用(没有catch的其它异常)存入局部变量4

26: ldc #27 // 将字符串常量"c"的引用压入栈顶
28: astore_1 // 将栈顶引用存入局部变量2,即 x = "c"

29: aload_3 // 将局部变量4压入栈顶
30: athrow // 抛出栈顶异常引用
Exception table:
from to target type
0 6 12 Class java/lang/IllegalArgumentException //如果catch的异常是IllegalArgumentException或其子类
0 6 25 any //如果catch的异常不是IllegalArgumentException
12 19 25 any //如果catch里面抛出异常
LineNumberTable:
line 14: 0
line 15: 3
line 20: 6
line 15: 9
line 16: 12
line 17: 13
line 18: 16
line 20: 19
line 18: 22
line 19: 25
line 20: 26
line 21: 29
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 this Lorg/eto/essay/Test;
3 9 1 x Ljava/lang/String;
16 9 1 x Ljava/lang/String;
29 2 1 x Ljava/lang/String;
13 12 2 e Ljava/lang/IllegalArgumentException;
StackMapTable: number_of_entries = 2
frame_type = 76 /* same_locals_1_stack_item */
stack = [ class java/lang/IllegalArgumentException ]
frame_type = 76 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]

}

从字节码可以看出,方法最终只能返回一个栈顶元素(不管是通过areturn还是athrow),也就是说,如果栈顶元素被覆盖了,将会丢失原本期望返回的值。比如在finally中抛出新的异常或者进行return


参考:

  1. Copyright ©《深入理解java虚拟机》