值得强调的是,Java虚拟机并不是Java语言所独自占有的,虚拟机不关心来源是何种语言。只要输入规范的统一的程序存储格式——字节码文件,就能产生需要的效果
Class类文件结构
首先把干货罗列出来
Class类文件是一种“高效率”的结构
Class文件是一组以8位字节为基础单位的二进制流,数据项目严格按照顺序紧密排列,Class文件中几乎所有内容都是程序运行必要的数据,没有空隙。
Class文件按照顺序,大致分为
- 魔数(4字节)
- 版本号(5,6字节)
- 常量池-访问标志(access_flags)
- 类索引、父索引、接口索引集合
- 字段表集合
- 方法表集合
- 属性表集合
Class文件采用类似C语言结构体的伪结构,只存储无符号数据,或无符号数据组成的集合——表
- 无符号数可以描述数字、引用、数量值或按照UTF-8编码构成的字符串值,u1,u2,u3,u8分别代表1,2,4,8个字节的无符号数
- 表由多个无符号数或者其他表构成的一种复合数据类型,以“_info”结尾,Class文件为一张表
类型 | 名称 | 中文名称 | 数量 |
---|---|---|---|
u4 | magic | 魔数 | 1 |
u2 | minor_version | 次版本号 | 1 |
u2 | major_version | 主版本号 | 1 |
u2 | constant_pool_count | 常量池容量计数值 | 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 | 接口 | interface_count |
u2 | fields_count | 字段数量 | 1 |
field_info | fields | 字段 | fields_count |
u2 | methods_count | 方法数量 | 1 |
method_info | methods | 方法 | methods_count |
u2 | attributes_count | 属性数量 | 1 |
attribute_info | attributes | 属性 | attributes_count |
到这里,很容易迷茫,这里我作出一张更为详细的表格方便理解
在阅读这个表格前,希望各位回忆一下Java对象的基本组成
public class MyClass extends Father implements Boy,Girl{
private String name;
public int age;
public void speak(){
}
}
Class文件是高效率存储程序信息的文件,一字不多、一字不少的存储了一个类
从下面的表中,你可以清晰的看到类的各个部分。
请先对该部分有一个印象,再继续阅读。
Class类文件的结构就是这样,下面详细的说说各个部分。
个人感觉后面的内容比较枯燥,使用生动的例子反而不是很清晰,若只是希望有一个了解,阅读至此,已经足以,若要对各个项目详细窥探,ways are yours
1、魔数与Class文件版本
打开你的Class文件,会发现开头为cafe,这就是魔数,0xCAFEBABE,这是Class文件的标识符
紧跟魔数的4个字节,为Class文件的版本号
JDK不能执行高于本版本的字节码文件,即使文件未变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
2、常量池
位置:主次版本号之后。
Class文件中的资源仓库,在Class文件中与其他项目关联最多(其他项目需要常量才能正确表达),占用了Class文件空间最大的数据项目之一
- 表类型数据项目
- 入口为一个计数值,从1开始而不是从0开始
- 当某个项目“不引用任何一个常量池项目”时,指向0
主要存放
- 字面量:接近常量,文本字符串、声明为final的常量值等等。
- 符号引用:各种
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
Java在编译时没有“连接”的步骤,Java通过在虚拟机加载Class文件时进行动态连接。Class文件中不会保存各个方法、字段的内存布局信息,不经过运行期转换的话无法得到真正的内存入口地址,无法被虚拟机使用。笔者认为这是Java的方法脱离了类,就不能使用的来源。
常量池中的每一项都是表类型(复合类型)
其中的项目开头第一位是一个u1类型(4位无符号数)的标志位(tag,取值如下)代表当前的常量类型。(JDK1.7前为11种,1.7后增加了三种)
类型 | 标志 | 描述 |
---|---|---|
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_MethodType_info | 16 | 标识方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 标识一个动态方法调用点 |
其中CONSTANT_Utf8_info类型
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
由于Class文件中方法、字段等都引用CONSTANT_Utf8_info型常量来描述,所以CONSTANT_Utf8_info的最大长度就是Java中方法、字段名的最大长度。即length的最大值,u2类型的最大值65535。所以超过64KB英文字符的变量或方法名,将无法编译。
3、访问标志
位置:常量池后紧接的两个字节
用于识别一些类或者接口层次的访问信息。
- 该Class类还是接口
- 是否定义为public
- 是否定义为abstract类型
- 是类的情况下,是否被定义为final
可以使用16个标志位,目前使用了8个
4、类索引、父类索引与接口索引集合
类索引和父类索引为u2类型,接口为一组u2类型数据的集合
这三项数据确定了该类的继承关系。
Object类的父类索引为0.
接口集合包括一个接口计数器,后接接口值的索引(真实值存储在常量池)
5、字段表集合
用于描述声明的变量。
字段表的结构为访问标志、名称的索引、参数信息构成的集合,不在详细的描述
值得一提的是,字段在类文件中,以简单名称进行储存(与全限定名相对,另外全限定名在存储时,点替换为斜杠)
虚拟机提供了描述标识符类型的标识字符
若字段是一个数组类型,在每一个维度前将放置一个“[”进行标识
如java.lang.Stringp[][]将记录为“[[Ljava/lang/String;”
另外,Java中,字段的名字不能重复,而对于字节码,若名字相同,类型描述符不同,则是合法的
6、方法表集合
方法表与字段表极为类似,
不过需要注意的是:方法表中只记录了方法的定义信息,而方法体存放在属性表中一个名为“Code”的属性中。
在Java中,重载(Overload,就是名字相同的那种)一个方法,除了相同的简单名称,还要求必须拥有一个与原方法不同的特征签名(Java中为方法名称、参数顺序、参数类型)。
因为在字节码文件中,特征签名多了方法的返回值以及受查异常表,所以在字节码中,重载的范围更宽了一些。
7、属性表集合
属性表的限制稍微宽松,不再严格要求各个项目的顺序,并且只要不与已知属性名重复,都可以写入自己定义的属性信息,Java虚拟机运行时会忽略不认识的属性。
下面给出预定义的一些属性表
其中,我们大致提出一些主要的属性
Code属性
Java程序的方法体经过Javac编译器处理后,最终转换为字节码存储在Code属性中,Code属性出现在方法表中的属性集合之中,但并非所有的方法表都必须有这个属性,比如接口和抽象类中的方法,就不存在Code属性,如果方法表有Code属性存在,那么他的结构如下
其中,
- 第一个u2 attriibute_name_index,指向了常量池中的一个utf8类型常量的索引,此常量固定值为Code
- attribute_length表示了属性值的长度。
- max_stack代表了操作数栈深度的最大值,在方法执行的任意时刻,操作数栈都不会超过这个深度,虚拟机在运行时根据这个值来分配栈帧中的操作树栈深度。
- max_locals代表了局部变量表所需的存储空间。
- 在这里,max_locals的单位是槽(slot),变量槽是虚拟机为局部变量分配内存的最小单位,对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,而double和long这种64位的数据类型则需要两个变量槽。
- 方法参数(包括实例方法中隐含的this)、显示异常处理程序的参数(在catch中定义的异常值)、方法体中定义的局部变量都依赖局部变量表来存放。
- 但是要注意,并不是方法中有多少个局部变量,就把这些局部变量表所占空间之和作为max_locals的值,操作数栈和局部变量表决定了一个方法的栈帧大小,不必要的数量会导致内存的浪费。
- Java虚拟机的做法是将局部变量表的局部变量池进行重用,当代码执行超过一个局部变量的作用域时,这个局部变量所占用的局部变量槽可以被其他局部变量使用,javac编译器会根据变量的作用域来分配变量槽来给各个变量使用,然后根据同时生存的最大局部变量数和类型来计算出max_locals的大小。
- code_length和code用来存储Java源程序编译后产生的字节码指令,code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。虽然code_length是一个u4类型的长度值,也就是理论上可以达到2的32次幂,但是根据java虚拟机规范,一个方法不允许超过65535条字节码指令,即实际值使用到了u2的长度。
在字节码指令之后的是显式异常处理表,异常表对于Code属性来说并不是必须存在的。其结构如图
字段的含义为,当字节码从start_pc行到end_pc行之间出现了类型为catch_type或者其子类的异常时,则转到第handler_pc行继续处理。 当catch_type的值为0时,代表任意异常情况都需要转到handler_pc行进行处理。
Exceptions属性
这个Exceptions属性是方法表中与Code属性同级的一项属性。作用是列举一个方法可能抛出的受检查异常(Checked Exceptions),也就是方法描述时,throws关键字后面列举的异常,其结构如下图
其中exception_index_table指向了常量池中CONSTANT_Class_info的索引。
LineNumberTable属性
LineNumberTable属性是用于描述Java源代码行号和字节码行号之间关系的。他不是运行时必须的,可以通过javac的参数 -g参数进行取消或生成。 如果取消这个属性,最大的影响就是在运行时抛出的异常不会包含报错行号,在调试程序时也无法根据源码行号来设置断点。
LocalVariableTable和LocalVariableTypeTable属性
LocalVariableTable属性是用来描述栈帧中局部变量表的变量与java源码中定义的变量之间的关系,也不是运行时必须的属性,如果不生成,最大的影响是对IDE工具调试时无法根据参数名从上下文中获取参数值。
在JDK5引入泛型后,LocalVariableTable属性增加了一个姐妹属性,LocalVariableTypeTable,其结构与LocalVariableTable很相似,仅仅是把描述字段的字段描述符替换成了字段的特征签名。由于描述符中泛型参数化类型被擦除,描述符不能准确描述泛型类型了,因此出现了LocalVariableTypeTable属性,使用特征签名来完成泛型的描述