之前还在美团实习的时候,当时读《深入理解Java虚拟机》由于时间原因只总结了几个章节,现在把余下的几个章节补充上,发表顺序有些混乱,章节主线详见文章汇总|学习Android的一点一滴。
本篇将介绍Class文件结构中的各个组成部分,以及每个部分的定义、数据结构和使用,有利于进一步了解虚拟机执行引擎。
- 概述
- 类文件结构
- 字节码指令
1.概述
运行在各种不同平台上的虚拟机通过载入和执行同一种平台无关的字节码来实现了程序的“一次编写,到处运行”。可见字节码是构成平台无关性的基石。
Java虚拟机不和Java等任何语言绑定,只和存储字节码的Class文件这种特定的二进制文件格式关联,且并不关心Class的来源是何种语言,也体现了Java虚拟机的语言无关性。
2.类文件结构
- Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间无任何分隔符,当遇到需要占用8位字节以上空间的数据项时,会按照高位在前的方式分割成若干个8位字节进行存储。
- Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,包含两种数据类型:
- 无符号数:属于基本数据类型;以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数;可用于描述数字、索引引用、数量值或按照UTF-8 编码构成的字符串值。
- 表:由多个无符号数或其他表作为数据项构成的复合数据类型;常以“_info”结尾;可用于描述有层次关系的复合结构的数据。
- 整个Class文件本质上就是一张表,所包含的数据项如图:
接下来依次介绍表中各个数据项的具体含义。
a. 魔数
- 魔数(Magic Number):每个Class文件的头4个字节
- 作用:判断该文件是否为一个能被虚拟机接受的Class文件
b.版本号
- 版本号:包含主版本号和一系列次版本号
- 次版本号(Minor Version):第5和第6个字节
- 主版本号(Major Version):第7和第8个字节
- 作用:判断该文件是否在虚拟机处理的有效范围内
c.常量池
- 常量池:使用一个前置的容量计数器(constant_pool_count)加上若干个连续的常量项(constant_pool)来描述
- 容量计数器:从1开始,目的是满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这时可以把索引值置为0来表示
- 常量项:如constant_pool_count=2表示常量池中有1个常量项
- 特点:是Class文件的资源仓库、是Class文件结构中与其他项目关联最多的数据类型、是占用Class文件空间最大的数据项目之一、是在Class文件中第一个出现的表类型数据项目
- 存放内容:两大类常量
- 字面量(Literal):指Java语言层面的常量概念,如文本字符串、声明为final的常量值等
- 符号引用(Symbolic References):指编译原理方面的概念,包含类和接口的全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符
Java代码进行Javac编译的过程同虚拟机加载Class文件的过程是动态连接的,因此在Class文件中不会保存各个方法、字段的最终内存布局信息,这就需要虚拟机在运行时从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
- 常量池中每一个常量都是一个表,详解见Class文件结构--常量池(一)
d.访问标志
- 访问标志(access_flags):常量池结束后两个字节
- 作用:识别一些类或者接口层次的访问信息,包括该Class是类还是接口、是否定义为public类型、是否定义为abstract类型、若是类是否被声明为final等。具体的标志位以及含义见图:
e.类索引、父类索引与接口索引集合
- 类索引(this_class)和父类索引(super_class)都是一个u2类型的数据、接口索引集合(interfaces)是一组u2类型的数据的集合
- 作用: 通过这三项数据来确定这个类的继承关系,具体的
- 类索引:确定这个类的全限定名
- 父类索引:确定这个类的父类的全限定名
- 接口索引集合:描述这个类所实现的接口,并按照implements语句后的接口顺序从左到右排列在接口索引集合中
- 接口索引集合的入口第一项u2类型数据为接口计数器(interfaces_count),从0计数,如nterfaces_count=2表示该类实现了两个接口
类全限定名:把类全名中的“.”都替换成“/”,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束
f.字段表集合
- 字段表(field_info):用于描述接口或者类中声明的变量
- 格式如图
- access_flags:存放字段的修饰符,具体的标志位以及含义见图:
- name_index:存放字段的简单名称,即没有类型和参数修饰的字段名称
- descriptor_index:存放字段和方法的描述符,包括字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值,具体的标志位以及含义见图:
- attribute_info:属性表见后
g.方法表集合
- 方法表(methods_info):用于描述接口或者类中声明的方法
- 格式如图,可见和描述字段的方式非常类似,仅在访问标志和属性表集合的可选项中有所区别。
h.属性表集合
- 属性表(attribute_info):用于描述某些场景专有的信息,在字段表、方法表等都携带自己的属性表集合
-
种类:
- 结构:属性名需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,属性值是自定义的、需要通过一个u4的长度属性说明属性值所占用的位数
3.字节码指令
- 构成:由一个字节长度的表示某种特定操作含义的操作(操作码、Opcode)和零至多个代表此操作所需的参数(操作数、Operands)构成
- 特点:非完全独立,即并非每种数据类型和每一种操作都有对应的指令,有些单独的指令可以在必要的时候用来将一些不支持的类型转换为可被支持的类型
- 分类:将字节码操作按用途大致分为9类
- 加载和存储指令:用于将数据在栈帧中的局部变量表和操作数栈之间来回传输
- 运算指令:用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶
- 类型转换指令 :用于实现用户代码中的显式类型转换操作,或者用于处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题
- 对象创建与访问指令:用于对象创建,并通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素
- 操作数栈管理指令:用于直接操作操作数栈
- 控制转移指令:用于从指定的位置有条件或无条件地进行指令
- 方法调用和返回指令:用于方法的调用,并根据返回值的类型去返回
- 异常处理指令:用于检测到异常状况时自动抛出异常
- 同步指令:用于方法内部一段指令序列的同步
具体指令见Java虚拟机字节码指令简介