zhaoyu@home:~$

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的限制。