《深入理解Java虚拟机》 类 & class文件
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
字段的简单名称分别为inc
和m
; - 全限定名:将类全名中的
.
替换成/
,并在最后添加一个;
表示全限定名结束; - 描述符:相对复杂一些,用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值;
描述符规则:
标识字符 | 含 义 | 标识字符 | 含 义 |
---|---|---|---|
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/String
,int[]
会被描述成[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的处理:
1 | public class Test { |
对于上面的代码可以知道:
- 如果
try
中没有异常,那么返回a
; - 如果
try
中出现IllegalArgumentException
或其子类异常,那么返回b
; - 如果
try
中出现非IllegalArgumentException
异常,那么方法异常退出;
下面通过其字节码看一下具体是如何实现的,关于Java字节码指令,可以参考:https://shanhm1991.github.io/2018/10/05/20181005/
1 | public java.lang.String test() throws java.io.IOException; |
从字节码可以看出,方法最终只能返回一个栈顶元素(不管是通过areturn还是athrow),也就是说,如果栈顶元素被覆盖了,将会丢失原本期望返回的值。比如在finally中抛出新的异常或者进行return。
参考:
- Copyright ©《深入理解java虚拟机》