一次编写,到处运行。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 | st=>start: 开始 |
字段表集合
字段表包含类字段和实例字段,不包含局部变量。包含以下信息。
- 字段作用域(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复制栈顶元素并将其压入栈顶。