jvm-class文件
ClassFile结构
一个class文件有一个ClassFile结构,如下:
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];
}
其中u1,u2,u4代表无符号的一个,两个或4个byte。
- magic
标识这是一个java的class文件,它的值为0xCAFEBABE,其他的值不被JVM认可。 - minor_version, major_version
该class文件的主,次版本号。不同的JVM实现支持一定范围的版本。如Oracle的JDK1.0.2中的JVM支持从45.0到45.3的版本。 - constant_pool_count
等于常量池长度加1。 - constant_pool[] 常量池是一个表,包含:字符串常量,类或者接口名称,字段名称,文件结构中引用的其他常量。index从1到constant_pool_count - 1。
- access_flags
表示文件的访问权限,代表的值如下:
ACC_SUPER表示使用invoespecial指令调用父类方法时特殊处理。主要为了向后兼容,java8以上默认所有class文件都被设置了ACC_SUPER。 ACC_SYNTHETIC标记一个类或接口是有编译期生成的,不存在源码。 - this_class
该值必须是常量池中一个可用的下标,并且常量池中这个下标存放的对象必须是CONSTANT_Class_info结构。 - super_class
该值必须是常量池中一个可用的下标或者为0。如果不为0,那么常量池中这个下标存放的对象必须是CONSTANT_Class_info结构。 为0时表示没有父类。 - interfaces_count
直接父接口的数量。 - interfaces[]
直接父接口数组,数组中每个值必须是在常量池中可用的下标。且常量池中的关联下标的值必须是CONSTANT_Class_info结构。 - fields_count
字段的数量。 - fields[]
字段数组,数组元素必须是field_info结构。只包括了本类或者接口定义的字段, 不包括父类和父接口中定义的。 - methods_count
方法的数量。 - methods[]
方法数组,数组元素必须是method_info结构,method_info包含了实例方法,类方法,实例初始化方法等。但是不包括父类方法和父接口的方法。 - attributes_count
属性的数量。 - attributes[]
属性的被ClassFile,filed_info,method_info和Code_attribute使用,如一个字段有多个属性,如常量字段有CONSTANT_Long, CONSTANT_Double等。 每个属性必须是attribute_info结构。
描述符
字段描述符
字段类型和类型的对比如下:
多维数组double[][][]
的类型描述为[[[D
。
方法描述
有如下方法:
Object m(int i, double d, Thread t) {...}
其描述为:
(IDLjava/lang/Thread;)Ljava/lang/Object;
常量池
JVM指令不依赖于运行时的类,接口,类实例,数组。相反,指令引用常量池(constant_pool)中的符号信息。所有的常量池有如下的格式:
cp_info {
u1 tag;
u1 info[];
}
每个常量池都有一个1byte大小的tag表示cp_info的种类,如下表:
具体每种种类的info结构,可以参考官方JVM文档说明。
字段
每个字段被描述为一个field_info结构。如下:
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
access_flags为访问标记,如下:
attributes的每个元素必须是attribute_info,存放方法的属性信息。
方法
每个方法,包括了每个实例的初始化方法或类和接口的初始化方法,都被描述为method_info结构。其组成如下:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
其中方法的access_flags如下:
属性
属性被用在ClassFile,field_info, method_info, 和 Code_attribute 结构中,如下:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
attribute_length的显示后续信息的byte长度。
共有23类属性,具体参考官方JVM文档说明。
格式检查
当一个class被JVM加载时,JVM要确保文件有类的基本格式。这个过程称为格式检查
。如下:
- 开始4个byte必须包含正确的魔法值。
- 所有的属性拥有正确的长度。
- class文件末尾必须没有被截断,也不能包含额外的长度。
- 常量池必须满足文档约束,如CONSTANT_Class_info必须含有指向常量池中CONSTANT_Utf8_info类型的name_index字段。
- 常量池中所有的字段引用和方法引用必须拥有可用的名称,可用的类,可用的描述。
格式检查只检查class文件的格式。不会检查字段,方法等是否真实存在。更细节的检查在字节码验证
阶段。
JVM Code约束
方法,实例初始化方法,类或者接口初始化方法的代码存储在code属性中的code数组中。code属性存放在class文件的method_info结构中,存放实现该方法 的真实的JVM指令。 下面我们主要讨论Code_attribute的内容相关的约束。
静态约束
class文件中代码的静态约束规定JVM指令在代码数组中如何分布,规定单个指令的操作数是什么。如下:
- code数组中第一个指令的操作码从索引0开始。
- code数组中,除了最后一个指令,下一个指令的操作码索引等于当前指令操作码的索引加上其长度。
- code数组中最后一个指令的最后一个byte必须是索引为code_length-1的byte。
- 跳转或者分支指令(goto,jsr,ifeq等)的目标必须是该方法内的一个指令的操作码。
- ldc指令的操作数必须是常量池表中的索引。 … …
- new指令指向一个CONSTANT_Class类型的常量池对象。
结构约束
code数组中的结构约束规定了JVM指令之间的关系。如:
- 每个指令必须在操作栈和本地变量表中有正确的类型的数量的参数。int类型的指令操作也适用于boolean,byte,char,short。
- 在执行过程中,不能从操作栈中弹出不存在栈中的元素。
- … …
- 本地变量在未赋值之前不能访问。
- aastore指令存储的数组元素必须是一个引用类型。
class文件的验证
即使java编译器产生的class文件满足了所有的静态和结构约束。JVM也不能确保所有需要加载的文件是由该编译器生成并格式正确。所以需要文件验证。
JVM是在链接阶段实现对class文件约束验证的。
链接时验证增强了运行时解释器的性能。这就避免了在运行时为每个解释指令验证约束的昂贵代价。JVM会假设这些检查已经被执行。如:
- 操作数栈不会上溢或者下溢。
- 所有的本地变量可以使用和存储。
- JVM指令的参数都是正确的类型。
有两种JVM的实现用于验证的策略: - 版本号不小于50的java文件必须执行类型检查验证。
- 类型推断验证必须被JVM实现支持,除了版本号小于50且遵循java ME CLDC和java Card 配置的。 但是还有3个额外的检查需要在验证阶段验证:
- 确保final类没有子类。
- 确保final方法没有覆盖。
- 检查除Object外的任何类都有一个父类。
类型检查验证
一个版本不小于50的class文件必须使用类型检查。如果版本是等于50的,那么在类型检查验证失败后,执行类型推断验证。 如果一个类的方法都是类型安全的,且它不是final类的子类,那么这个类时类型安全的。类型检查使用Prolog语句的方式表示规则。
classIsTypeSafe(Class) :-
classClassName(Class, Name),
classDefiningLoader(Class, L),
superclassChain(Name, L, Chain),
Chain \= [],
classSuperClassName(Class, SuperclassName),
loadedClass(SuperclassName, L, Superclass),
classIsNotFinal(Superclass),
classMethods(Class, Methods),
checklist(methodIsTypeSafe(Class), Methods).
classIsTypeSafe(Class) :-
classClassName(Class, 'java/lang/Object'),
classDefiningLoader(Class, L),
isBootstrapLoader(L),
classMethods(Class, Methods),
checklist(methodIsTypeSafe(Class), Methods).
classClassName:提取类的名称。classIsInterface类是否是一个接口
验证类型系统
类型检查器基于验证类型形成了一个类型系统。如下:
- 基本类型double,float,int和long对应相同名称的验证类型。
- 基础类型byte,char,shrot和boolean对应int的验证类型。
- 类和接口的验证类型对应class。验证类型class(N,L)表示二进制名为N被类加载器L加载的class。注意L是类的初始化加载器,可能不是class定义的
加载器。如Object被表示为
class('java/lang/Object',BL)
,而BL是bootstrap loader。 - 数组对应的验证类型为arrayOf。使用arrayOf(T)表示数组元素为T。如int[]表示为arrayOf(int)。
验证类型uninitialized(Offset)
表示一个应用了数字类型的offset参数的uninitialized。
其他验证类型使用Prolog表示为名称和验证类型相关的原子。
验证类型的子类型规则如下,即判定验证类型之间的父子关系:
子类型具有自反性。
isAssignable(X,X)
验证类型不同于java语言的引用。具有如下规则,如果v可以被赋值给X,那么v的直接父类型(必须是直接父类型)也可以被赋值给X。
isAssignable(v, X) :- isAssignable(the_direct_supertype_of_v, X).
v的直接父类型是X的子类型,那么v也是x的子类型。规则如下:
isAssignable(oneWord, top). //oneWord可以被赋值给top
isAssignable(twoWord, top). //twoWord可以被赋值给top.
isAssignable(int, X) :- isAssignable(oneWord, X). //int可以赋值给X,得出oneWord也可以被赋值给X
isAssignable(float, X) :- isAssignable(oneWord, X).
isAssignable(long, X) :- isAssignable(twoWord, X).
isAssignable(double, X) :- isAssignable(twoWord, X).
isAssignable(reference, X) :- isAssignable(oneWord, X).
isAssignable(class(_, _), X) :- isAssignable(reference, X).
isAssignable(arrayOf(_), X) :- isAssignable(reference, X).
isAssignable(uninitialized, X) :- isAssignable(reference, X).
isAssignable(uninitializedThis, X) :- isAssignable(uninitialized, X).
isAssignable(uninitialized(_), X) :- isAssignable(uninitialized, X).
isAssignable(null, class(_, _)).
isAssignable(null, arrayOf(_)).
isAssignable(null, X) :- isAssignable(class('java/lang/Object', BL), X), isBootstrapLoader(BL).
类型推断验证
不包含StackMapTable属性的class文件(版本小于50)需要使用类型推断验证。
类型推断验证的过程
在链接阶段,验证器通过在每个方法上执行数据流分析检查code数组中的指令。验证器确保在程序的任何点,不管通过任何代码路径到达,以下规则为真:
- 操作栈总是大小一样,并且包含相同类型的值。
- 本地变量直到包含一个正确类型的值后才可访问。
- 方法的调用参数正确。
- 字段只能使用规定的类型赋值。
- 所有的操作码在操作栈和本地变量表中拥有正确的类型的参数。
字节码验证
每个方法的代码指令是被独立验证的。代码的字节被划分为一系列指令。每个指令需要检查可用性。如:
- 代码的分支必须也在方法的代码数组内。
- 控制流程指令的目标是另一个指令的开始。
- 不能有指令访问和修改超出方法分配范围的本地变量。
- 所有的常量池引用必须有正确的类型。
- 代码不能在指令中间结束。
- 执行在代码结束前不能失败。
- 对于异常捕获,处理器的代码保护的开始和结束必须是指令的开始。开始点必须在结束点之前。
JVM的一些限制
- 每个类或者接口的常量池上限为65535条。由class结构中的constant_pool_count的16bit字段规定。
- fields_count规定了类或者接口的字段最多为65535条,不包括父类或父接口继承的字段。
- methods_count 限制了方法最多为65535。
- 本地变量的个数最多也为65535,注意long和double消耗两个单位。
- 栈帧中操作栈的大小也为65535。
- 方法参数个数的限制为255。
- 字段、方法的名称,字段、方法的描述,其他常量字符串值限制为65535个字符。因为CONSTANT_Utf8_info结构有u2长度的限制。
- 数组的维度有255的限制。