虚拟机执行子系统(一):类文件结构
Class文件结构
-
一个Class文件对应唯一类或接口,但反过来则不是(譬如类或接口也可以动态生成,直接送入类加载器中)。
-
Class文件是一组以8个字节为基础单位的二进制流,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。需要占用8个字节以上空间的数据项按照高位在前的方式分割(Big-Endian:高字节在低地址,与x86架构相反)。
Class的结构不像XML等描述语言,无论是顺序还是数量,甚至于数据存储的字节序(Byte Ordering,Class文件中字节序为Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,全部都不允许改变。
-
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:“无符号数”和“表”。
-
无符号数:为基本数据类型(以
u1
、u2
、u4
、u8
来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。) -
表:由多个无符号数或者其他表作为数据项的复合数据类型,以
_info
结尾
这两种数据类型需要描述数量不定的多个数据的时候,会使用一个前置的容量计数器加上若干个连续数据项的形式,称这一系列连续的某一类型的数据为某一类型的“集合”。
-
魔数与Class文件版本
- 魔数:Class文件的头四个字节,用于标识该文件是否能被JVM接受(表示这个文件是否是
.class
文件,值为0xCAFEBABE
咖啡宝贝) - 版本号:紧跟魔数之后的4个字节,5、6为次版本号,7、8为主版本号(目前只考虑主版本号的话是45.x-57.x),低版本的JVM无法执行高版本的
.class
文件
常量池
紧跟主次版本号,占用Class文件空间最大的数据项目之一。
-
入口u2类型的数据——常量池容量计数值(constant_pool_count):从1开始计数,空出第0个常量(目的:后面某些指向常量池的索引值的数据在特定情况下“不引用任何一个常量池项目”),一共有
constant_pool_count - 1
个常量 -
存放两大常量:
-
字面量
-
符号引用:主要包括如下几类常量
-
被模块导出或者开放的包(Package)
-
类和接口的全限定名(Fully Qualified Name)
-
字段的名称和描述符(Descriptor)
-
方法的名称和描述符
-
方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
-
动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
.java
文件由javac编译为字节码时,没有进行“连接”步骤,直到JVM加载.class
文件时才进行动态连接(把各个方法和字段最终置入内存,即标明这些方法和字段的内存入口地址(符号引用更改为实际的内存地址))
-
-
-
每一项常量都是一个表:
表内结构:起始第一位是个
u1
类型的标志位,用于标识当前表对应的常量类型。-
CONSTANT_Class_info
(类/接口名称)name_index
是常量池的索引值,指向常量池中一个CONSTANT_Utf8_info
类型常量(代表这个类或者接口的全限定名) -
CONSTANT_Utf8_info
(UTF8编码的字符串类型)其中
length
字段表示的是使用UTF8缩略编码表示的字符串。UTF-8缩略编码与普通UTF-8编码的区别是:
从'\u0001'
到'\u007f'
之间的字符(相当于1~127的ASCII码)的缩略编码使用一个字节表示,从'\u0080'
到'\u07ff'
之间的所有字符的缩略编码用两个字节表示,从'\u0800'
开始到'\uffff'
之间的所有字符的缩略编码就按照普通UTF-8编码规则使用三个字节表示这个类型的字段由于
length
为u2
类型,所以他也限制了Java中方法、字段名的最大长度:65535(2字节) -
剩下的其他表
可以使用
javap
命令来分析字节码文件package org.fenixsoft.clazz; public class TestClass { private int m; public int inc() { return m + 1; } }
C:\>javap -verbose TestClass Compiled from "TestClass.java" public class org.fenixsoft.clazz.TestClass extends java.lang.Object SourceFile: "TestClass.java" minor version: 0 major version: 50 Constant pool: const #1 = class #2; // org/fenixsoft/clazz/TestClass const #2 = Asciz org/fenixsoft/clazz/TestClass; const #3 = class #4; // java/lang/Object const #4 = Asciz java/lang/Object; const #5 = Asciz m; const #6 = Asciz I; const #7 = Asciz <init>; const #8 = Asciz ()V; const #9 = Asciz Code; const #10 = Method #3.#11; // java/lang/Object."<init>":()V const #11 = NameAndType #7:#8;// "<init>":()V const #12 = Asciz LineNumberTable; const #13 = Asciz LocalVariableTable; const #14 = Asciz this; const #15 = Asciz Lorg/fenixsoft/clazz/TestClass;; const #16 = Asciz inc; const #17 = Asciz ()I; const #18 = Field #1.#19; // org/fenixsoft/clazz/TestClass.m:I const #19 = NameAndType #5:#6; // m:I const #20 = Asciz SourceFile; const #21 = Asciz TestClass.java; # 其中#加数字的组合表示符号引用的值
方法描述符:由方法的参数类型以及返回类型所构成。
-
访问标志
紧跟常量池,长度为两个字节,这些标志用于识别一些类或者接口层次的访问控制信息,包括
- 这个Class是类还是接口
- 是否是
public
类型 - 是否定义为
abstract
类型 - 如果是类,是否声明为
final
类索引、父类索引与接口索引集合
-
概念
- 这三个数据用于确定该类的继承关系
- 类索引:用于确定该类的全限定名
- 父类索引:确定这个类父类的全限定名,只允许有一个(不允许多继承),除了
java.lang.Object
外,所有Java类的父类索引都不为0 - 接口索引集合:描述该类实现了哪些接口,并按
implements
关键字(或者当前接口的extends
)后的接口顺序从左到右排列在接口索引集合中
-
类索引和父类索引的查找
首先从类索引开始,他的值为一个
CONSTANT_Class_info
的表,其中CONSTANT_Class_info
的值指向了一个CONSTANT_Utf8_info
的表,这个表中含有类的全限定名 -
接口索引集合
入口的第一项u2类型的数据为接口计数器(
interfaces_count
),从0开始计算,后面跟有接口索引表
字段表集合
第一个u2
类型的数据表示字段表集合的容量(fields_count
)
-
概念
-
用于描述接口或者类中声明的变量(Java语言中的“字段”(Field)包括类变量、实例变量,但不包括在方法内部声明的局部变量。)
字段包括的修饰符有:
- 字段的作用域(
public
、private
、protected
修饰符) - 是实例变量还是类变量(
static
修饰符) - 可变性(
final
) - 并发可见性(
volatile
修饰符,是否强制从主内存读写) - 可否被序列化(
transient
修饰符) - 字段数据类型(基本类型、对象、数组)
- 字段名称
字段的名字、数据类型只能引用常量池中的常量来描述
上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。
-
access_flags
用u2
类型来表示,其标志位如下 -
name_index
和descriptor_index
都是对常量池的引用,分别代表字段的简单名称以及字段的和方法的描述符-
简单名称:没有类型、参数修饰的方法或者字段名称(比如方法
int getNum(Item[] items)
,简单名称就是getNum
,字段private Item item
的简单名称就是item
) -
描述符:
-
字段描述符:字段的数据类型
-
方法描述符:方法的参数列表(数量、类型、顺序)+方法返回值(先参数列表后返回值存储)
方法
int indexOf(char[]source,int sourceOffset,int sourceCount,char[]target,
int targetOffset,int targetCount,int fromIndex)
的描述符为“([CII[CIII)I
” -
基本数据类型以及
void
用一个大写字符表示、对象类型用L+对象全限定名表示
-
-
-
后面跟随的属性表集合用于存储额外信息,比如“
final static int m=123;
”,那就可能会存在一项名称为ConstantValue
的属性,其值指向常量123
。 -
有可能出现不在原本Java代码中的字段,比如内部类存有指向外部类实例的引用
- 字段的作用域(
-
方法表集合
-
概念
方法表集合和字段表集合十分相似,第一个
u2
类型的数据表示方法表集合的容量(methods_count
)方法表和字段表结构十分相似
-
access_flags
内容如下 -
方法中的代码存放在方法属性表集合中一个名为
Code
的属性里面 -
有可能出现不在原本Java代码中的字段,比如类构造器“
<clinit>()
”和实例构造器“<init>()
” -
Java语言层面的重载,如果方法签名(由方法的名称和参数类型构成)仅仅是返回值不同,也是不允许的,Class文件层面的重载只要字节码方法签名(方法名称、方法参数、方法返回值以及受查异常表)不完全相同即可
-
属性表集合
JDK12中的预定义属性如下:
- 属性的名称:常量池中的一个
CONSTANT_Utf8_info
类型的常量 - 属性值的结构:完全自定义,只需要通过一个
u4
来说明属性值所占位数
-
Code属性
方法体中的代码编译为字节码后,存储到这个属性中。
但并非所有的方法表都有这个属性,例如接口或者抽象类的方法就不存在该属性。
-
attribute_name_index
:属性名,指向一个CONSTANT_Utf8_info
常量,值固定为Code -
attribute_length
:属性值长度 -
max_stack
:操作数栈的最大深度,JVM根据这个值分配栈帧的中操作栈的深度 -
max_locals
:局部变量表所需的存储空间,单位是变量槽(Slot
)。- 基本数据类型和
returnAddress
这些长度不超过32bit的:1Slot
- double、long,64bit:2
Slot
总的来说,方法参数(包括实例隐藏参数
this
)、显式异常处理程序参数(catch
的参数)、方法体中定义的局部变量都需要局部变量表来存放。JVM将局部变量表中的变量槽进行重用:超出作用域时,新的局部变量可以使用以前局部变量的槽。
Javac编译器根据同时生存的最大局部变量数量和类型计算出
max_locals
- 基本数据类型和
-
code_length
:字节码长度,u4
类型,但是《JVM规范》规定一个方法不能超过65535条指令,实际只能使用u2
的长度 -
code
:字节码指令,每个指令为u1
类型的单字节(最多可以表示256条指令)package org.fenixsoft.clazz; public class TestClass { private int m; public int inc() { return m + 1; } }
上面的程序在字节码中是这样的
- 读入2A,查表得0x2A对应的指令为
aload_0
,这个指令的含义是将第0个变量槽中为reference类型的本地变量推送到操作数栈顶。 - 读入B7,查表得0xB7对应的指令为
invokespecial
,这条指令的作用是以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private
方法或者它的父类的方法。这个方法有一个·类型的参数说明具体调用哪一个方法,它指向常量池中的一个CONSTANT_Methodref_info
类型常量,即此方法的符号引用。 - 读入000A,这是
invokespecial
指令的参数,代表一个符号引用,查常量池得0x000A对应的常量为实例构造器“<init>()
”方法的符号引用。 - 读入B1,查表得0xB1对应的指令为
return
,含义是从方法的返回,并且返回值为void
。这条指令执行后,当前方法正常结束。
从上述过程可以看出,JVM执行字节码基于栈的体系结构
// 原始Java代码 public class TestClass { private int m; public int inc() { return m + 1; } } C:\>javap -verbose TestClass // 常量表部分的输出省略掉 { public org.fenixsoft.clazz.TestClass(); Code: Stack=1, Locals=1, Args_size=1 #//在这里Locals=1和Arg_size=1是因为实例方法含有this指针 0: aload_0 1: invokespecial #10; //Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lorg/fenixsoft/clazz/TestClass; public int inc(); Code: Stack=2, Locals=1, Args_size=1 0: aload_0 1: getfield #18; //Field m:I 4: iconst_1 5: iadd 6: ireturn LineNumberTable: line 8: 0 LocalVariableTable: Start Length Slot Name Signature 0 7 0 this Lorg/fenixsoft/clazz/TestClass; }
- 读入2A,查表得0x2A对应的指令为
-
显式异常处理表:不一定存在
表示从
start_pc
到end_pc
之间,如果出现了类型为catch_type
(指向一个CONSTANT_Class_info
型常量)或者其子类的异常,则转到handler_pc
行继续处理。特别的,当catch_type == 0
时,任何异常都转到handler_pc
行继续处理。Java异常处理
finally
中的代码总会被执行。- 当
try
、catch
中有return
时,也会执行finally
。return
的时候,要注意返回值的类型,是否受到finally
中代码的影响。 finally
中有return
时,会直接在finally
中退出,导致try
、catch
中的return
失效。
-
-
Exceptions属性
-
作用:列举出方法中可能抛出的受查异常(Checked Excepitons),也就是方法描述时在
throws
关键字后面列举的异常。 -
结构:
number_of_exceptions
:可能抛出的异常种类数量exception_index_table
:异常类型,指向常量池中CONSTANT_Class_info
型常量
-
-
LineNumberTable属性
-
作用:用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。不是运行时必需的(默认生成到class文件中),主要用于调试
-
结构:
line_number_info
:包含start_pc
(字节码行号)与line_number
(Java源码行号)两个u2
数据项
-
-
LocalVariableTable及LocalVariableTypeTable属性
-
LocalVariableTable作用:描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性(默认生成到class文件中),也是跟调试有关,如果没有这个属性,则无法通过参数名称查找参数。
-
LocalVariableTable结构
local_variable_info
项目代表了一个栈帧与源码中的局部变量的关联-
start_pc
:这个局部变量的生命周期开始的字节码偏移量 -
length
:该局部变量作用范围长度两者合起来便是该局部变量的作用域
-
name_index
:局部变量的名称,常量池中CONSTANT_Utf8_info
型常量 -
descriptor_index
:局部变量的描述符,常量池中CONSTANT_Utf8_info
型常量 -
index
:局部变量在栈帧的局部变量表中变量槽的位置。
-
-
LocalVariableTypeTable:和LocalVariableTable很相似,只是把
descriptor_index
替换为了字段特征签名Signature
,对于非泛型类型,这个改动没有影响;对于泛型类型,必须使用这个属性以保存泛型的参数化类型。
-
-
SourceFile及SourceDebugExtension属性
-
SourceFile作用:记录生成这个Class文件的源码文件名称,对于内部类其源码文件名和类名不一定相同
-
SourceFile结构
-
SourceDebugExtension作用:存储额外的代码调试信息,典型的场景是在进行JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号。跟JSR45有关,主要用于定位非Java语言但需要用JVM运行的程序。
-
-
ConstantValue属性
-
作用:通知JVM自动为静态变量(static修饰)赋值
实例变量和静态变量的赋值时刻不同:
- 实例变量:在实例构造器
<init>()
中进行 - 静态变量:如果是基本类型或者
String
类型,且有final
修饰,则自动生成ConstantValue
属性来初始化;否则(不满足上述两个条件任一),则会在类构造器<clinit>()
进行
- 实例变量:在实例构造器
-
结构
其中
attribute_length
是固定值,为2
(后面的属性长度),constantvalue_index
是常量池中一个字面量的引用(基本类型到CONSTANT_String_info
)
-
-
InnerClasses属性
-
作用:记录内部类和宿主类之间的关联
-
结构:
-
number_of_classes
:内部类的数量 -
inner_classes_info
:内部类的信息表inner_class_info_index
和outer_class_info_index
都是指向常量池中CONSTANT_Class_info
型常量的索引,分别代表了内部类和宿主类的符号引用。inner_name_index
是指向常量池中CONSTANT_Utf8_info
型常量的索引,代表这个内部类的名称,如果是匿名内部类,这项值为0。inner_class_access_flags
是内部类的访问标志,类似于类的access_flags
-
-
-
Deprecated及Synthetic属性
都是布尔属性(要么有则为
true
,要么根本没有,可以看作是false
)。- Deprecated:通过
@deprecated
配置 - Synthetic:代表此字段不是Java源码产生,而是由编译器自动添加的
attribute_length
为0x00000000
- Deprecated:通过
-
StackMapTable属性
存在于Code属性中会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
-
Signature属性
是一个可选的定长属性,可以出现于类、字段表和方法表结构的属性表中。
-
作用:用于记录泛型签名信息,因为Java的泛型会擦除,这个属性使得Java支持通过反射获取泛型类型。
-
结构
signature_index
:对常量池的索引(CONSTANT_Utf8_info
),表示类签名或方法类型签名或字段类型签名(具体是哪个的签名,取决于Signature
属性是哪个表的属性)
-
-
BootstrapMethods属性
是一个复杂的变长属性,位i于类文件的属性表中。
-
作用:保存
invokedynamic
指令引用的引导方法限定符。(跟反射有关) -
结构
-
-
MethodParameters属性
是一个用在方法表中的变长属性。
-
作用:记录方法的各个形参名称和信息,因为接口和抽象方法没有Code属性,Javac加上
-parameters
参数即可。 -
结构
name_index
是一个指向常量池CONSTANT_Utf8_info
常量的索引值,代表了该参数的名称。而access_flags
是参数的状态指示器,它可以包含以下三种状态中的一种或多种:0x0010
(ACC_FINAL
):表示该参数被final修饰。0x1000
(ACC_SYNTHETIC
):表示该参数并未出现在源文件中,是编译器自动生成的。0x8000
(ACC_MANDATED
):表示该参数是在源文件中隐式定义的。Java语言中的典型场景是this
关键字。
-
-
模块化相关属性
模块描述文件(module-info.java)最终是要编译成一个独立的Class文件来存储的,所以,Class文件格式也扩展了Module、ModulePackages和ModuleMainClass三个属性用于支持Java模块化相关功能。
-
运行时注解相关属性
一共有6个属性用于存储注解信息。
RuntimeVisibleAnnotations
:记录了类、字段或方法的声明上记录运行时可见注解,当我们使用反射API来获取类、字段或方法上的注解时,返回值就是通过这个属性来取到的。type_index
是一个指向常量池CONSTANT_Utf8_info
常量的索引值,该常量应以字段描述符的形式表示一个注解。num_element_value_pairs
是element_value_pairs
数组的计数器,element_value_pairs
中每个元素都是一个键值对,代表该注解的参数和值。