Class类文件数据结构(卷三)

一次编写,到处运行。Write Once,Run Anywhere。

一次编写,到处运行。Write Once,Run Anywhere。虚拟机旨在实现与平台无关,同时,Java在成立之初就承诺在未来也会实现与语言无关。二者的基础都是虚拟机和字节码。

Class文件可以以任何形式存在,如存在磁盘,通过类加载器直接生成etc。Class文件采用一种类似于C中的结构体来存储数据,该伪结构体中只存在两种数据类型A无符号数和B表。其中无符号数有u1,u2,u4,u8;代表1、2、4、8个字节。而表是由多个无符号数和其他表组成的复合数据类型。常以_info结尾。整个Class文件本质上就是一张表。

Class文件格式

类型 名称 数量
u4 magic魔数 1
u2 minor_version次版本 1
u2 major_version主版本 1
u2 constant_pool_count常量池个数(入口,从1开始计数) 1
cp_info constant_pool常量池 constant_pool_count -1
u2 access_flags访问标识 1
u2 this_class类索引 1
u2 super_class父类索引 1
u2 interfaces_count接口索引个数 1
u2 interfaces接口列表 interfaces_count
u2 fields_count字段个数 1
field_info fields字段集合 fields_conut
u2 methods_count方法个数 1
method_info methods方法集合 methods_count
u2 attributes_count属性个数 1
attribute_info attributes属性集合 attributes_count

常量池

只有常量池的表是从1开始计数的,其中的0是用于特殊考虑的,表达不引用常量池中任一对象。
常量池中主要存放两大常量,一是字面量(Literal),如文本字符串,final的常量值。二是符号引用(Symbolic Reference),包含3类,A类和接口的全限定名(Fully Qualified Name);B字段的名称和描述符(Descriptor);C方法的名称和描述符。

在Class文件中不好保存各方法、字段的最终内存布局信息,而是等到虚拟机运行时,从常量池中得到符号引用,再在类创建或者运行时解析到具体的内存地址中。换句话说,Class文件不经过运行时转换的话,是无法直接在虚拟机中使用的。

常量池中的每一项常量都是一个表,常用的有14个表,这14个表的共性是开始的第一位都是一个u1类型的tag标识。下表是常量池中的tag类型。

类型 tag标识 描述
CONSTANT_Utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整型字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整型字面量
CONSTANT_Double_info 6 双精度浮点型字面量
CONSTANT_Class_info 7 类或者接口的符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的部分符号引用
CONSTANT_MethodHandle_info 15 方法句柄
CONSTANT_Method_info 16 方法类型
CONSTANT_InvokeDynamic_info 18 一个动态方法调用点

对于64k问题,原因是tag=1的常量的最大长度就是u2的长度即65535.

访问标识access_flags

该标识用于识别类或者接口的访问信息,例如这个Class是类还是接口;是否定义为public;是否定义为abstrace;如果是类,是否定义为final等。一般以ACC_开头。最终结果为各个标识的位或结果。

类索引、父类索引、接口索引

三者共同确定了该类的继承关系。下图显示了类索引和父类索引寻找类全限定名字符串。

1
2
3
4
5
6
7
8
st=>start: 开始
op1=>operation: 找到类索引(指向this_class或super_class)
op2=>operation: 找到该索引指向的CONSTANT_Class_info
op3=>operation: 找到CONSTANT_Class_info中的index指向CONSTANT_Utf8_info
op4=>operation: 找到CONSTANT_Utf8_info中的常量中的全限定名字符串。
e=>end

st->op1->op2->op3->op4->e

字段表集合

字段表包含类字段和实例字段,不包含局部变量。包含以下信息。

  • 字段作用域(public、protected、private)
  • 是类变量还是实例变量(static)
  • 可变性(final)
  • 并发可见性(volatile是否从主内存直接读写)
  • 可序列化(transient)
  • 字段类型(基本类型、对象、数组)

字段表结构

类型 名称 数量
u2 access_flags字段访问标识 1
u2 name_index字段简单名称 1
u2 descriptor_index字段描述符 1
u2 attributes_count属性个数 1
attr_info attributes属性表 attributes_count

描述符标识字符含义

标识符号 含义
B byte
C char
D double
F float
I int
J long
S short
Z boolean
V void
L 对象类型

描述符采用先参数列表再返回值的形式。如([[CII)V标识参数为char二维数组以及两个int,返回值为void。

在这之后为属性表集合,用于存储一些额外的信息。同时,字段表中不会列出从父类中继承的字段,但是有可能自动生成新的字段,如内部类中会添加指向外部类实例的对象。另外,在Java中,字段的名称必须不一样;但是在字节码中,只要两个字段描述符不一致,字段可以重名

方法表集合

方法表和字段表设计上大都相同,只在访问标识和属性表集合中的可选项有些区别。

  • 方法中的Java代码,编译成字节码指令后,存储在方法属性表集合中一个叫”Code”的属性里。
  • 在Java语言中,因为返回值不会包含在特征签名中,所以无法只通过返回值对方法进行重载。
  • 在字节码里,特征签名范围大一些,可以只通过返回值在Class文件中共存两个名称相同的方法。

属性表集合

属性表用于描述某些场合专有的信息。下面列出一些比较重要的虚拟机预定义的属性。

属性名称 使用场景 含义
Code 方法表 编译后的字节码指令
ConstantValue 字段表 final关键字定义的常量值
LocalVariableTable Code属性 方法的局部变量描述
Signature 类、方法表、字段表 支持泛型下的签名,避免类型擦除导致的混乱,需要该属性记录泛型相关信息。

Code属性表结构

类型 名称 数量
u2 attribute_name_index(指向C_Utf8,恒为“Code”) 1
u4 attribute_length 1
u2 max_stack操作数栈最大深度 1
u2 max_locals局部变量表的最大存储空间(实例方法中方法参数至少为1,为隐式this) 1
u1 code_length 1
u4 code(二者共同编译后的字节码指令,虽然code为u4,但是虚拟机明确规定一个方法的字节码指令数不超过65535,即实际上只有u2个) code_length
u2 exception_table_length 1
exception_info exception_table异常表集合 exception_table_length
u2 attributes_count 1
attribute_info attributes attributes_count

字节码指令

  • 加载(load)和存储(store)用于在局部变量表和操作数栈间传输数据。load(局部->栈),store(栈->局部)。
  • 常量加载到操作数栈。Tipush,ldc,Tconst。
  • pop弹出栈顶元素。
  • dup复制栈顶元素并将其压入栈顶。