Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8个字节进行存储
在字节码结构中,有两种最基本的数据类型来表示字节码文件格式,分别是:无符号数和表
-
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。 -
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以**“_info”结尾**。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表,这张表由下图所示的数据项按严格顺序排列构成
在Class类文件中,无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的“集合”
在讲 Class类文件 结构之前我先来贴一段代码,对于一个类的存储有个大致印象即可,后续会一一讲到
package com.xxx;
import java.io.Serializable;
public class Demo implements Serializable {
public final static String final_static_jvm = "final_static_name";
private static String static_jvm = "static_jvm";
protected final String final_jvm = "final_jvm";
public String jvm = "jvm";
public static void main(String[] args) {
System.out.println(final_static_jvm);
System.out.println(demoMethod());
}
private static String demoMethod(){
return static_jvm + "_demoMethod";
}
}
我们可以用 javac 编译 java文件,用 javap -verbose 反编译 class 文件
1. magic 魔数 - u4
每个Class文件的头4个字节被称为魔数,固定值 0xCAFEBABE
唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件 (类加载的验证阶段)
补充:
- 很多文件格式标准中都有使用魔数来进行身份识 别的习惯,譬如图片格式,如GIF或者JPEG等在文件头中都存有魔数
- 使用魔数而不是扩展名来进行识别主要是基于安全考虑,因为文件扩展名可以随意改动
2. minor_version 次版本号 - u2 与 major_version 主版本号 - u2
(类加载的验证阶段) 高版本的JDK能向下兼容以前版本的Class文件,但不能运行之后版本的Class文件
3. 常量池
常量池可以比喻为Class文件里的资源仓库,它是Class 文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一
在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始。
- 举例:匿名内部类本身没有类名称,进行名称引用时会将index指向0;Object类的文件结构的父类索引指向0;
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
- 字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。
- 符号引用则属于编译原理方面的概念,主要包括下面几类常量:
- 被模块导出包
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 方法句柄和方法类型
- 动态调用点和动态常量
在虚拟机加载Class 文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段最终在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址,也就无法直接被虚拟机使用的。当虚拟机做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中
常量池中每一项常量都是一个表
这20个表又可以细分:
由上图可知:方法名称和字段名称都是存储于常量池中的。而存储这两个名称需要用到常量池中的 CONSTANT_Utf8_info 类型来存储,由上表 CONSTANT_Utf8_info 的 u2 length 可知 方法名称和字段名称长度限制是 2的16次方 = 65535 = 64kb,Java程序中如果定义了超过64KB英文字 符的变量或方法名,即使规则和全部字符都是合法的,也会无法编译
4. 访问标志 access_flags - u2
这个标志用于识别一些类或者接口层次的访问信息
- 包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final
5. 类索引、父类索引与接口索引集合
类索引 this_class和父类索引 super_class都是一个u2类型的数据,而接口索引集合 interfaces是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类型的继承关系
-
类索引用于确定这个类的全限定名(全限定名称存储于常量池) -
父类索引用于确定这个类的父类的全限定名。(全限 定名称存储于常量池)。这里说的索引,是指向常量池中的constant_class_info类型,constant_class_info 又指向了 constanct_utf8_info 从而最终找到全限定名。 -
接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字(如果这个Class文件表示的是一个接口,则应当是extends关键字)后的接口顺序从左到右排列 在接口索引集合中。
- 对于接口索引集合,入口的第一项u2类型的数据为接口计数器(interfaces_count),表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节
由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了 java.lang.Object外,所有Java类的父类索引都不为0
类索引查找全限定名的过程
类索引和父类索它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过 CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串
6. 字段表集合 field_info - u2
用于描述接口或者类中声明的变量(Java语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量)
字段表存储的其实是变量(非局部变量)的修饰符 + 字段描述符索引(索引指向常量池) + 字段名称 索引(索引指向常量池)+ 属性表
- 字段可以包括的修饰符有字段的作用域(public、private、protected修饰 符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否 强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、 字段名称
public final static String NUMBER="1"
- pulic final 和static 是字段的修饰符,这些都存放在class文件的字段表中。
- String是字段的描述符,存放于常量池中
- Number是字段的名称,存放于常量池。
这两部分的关联,是通过字段表的name_index指向常量池中的字段名称number 和 descrip_index 指向常量池中的描述符。
描述符的作用是用来描述字段 的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类 型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大 写字符来表示,而对象类型则用字符L加对象的全限定名来表示
7. 方法表集合 methods_count - u2 与 methods
Class文件存储格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,方法表的结构如同字段表一样, 依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合 (attributes)、几项。
因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了 ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对,synchronized、native、strictfp和abstract 关键字可以修饰方法,方法表的访问标志中也相应地增加了ACC_SYNCHRONIZED、 ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志
方法里的Java代码,经过Javac编译器编译成字节码指令之 后,存放在方法属性表集合中一个名为“Code”的属性里面
与字段表集合相对应地,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出 现来自父类的方法信息。但同样地,有可能会出现由编译器自动添加的方法,最常见的便是类构造 器“<clinit>()”方法和实例构造器“<init>()”方法
- “<clinit>()”方法 其实就是针对静态变量和静态方法的
- “<init>()”方法 是值的是实力构造器
8. 属性表集合
属性表(attribute_info),Class文件、字段表、方法表都可以 携带自己的属性表集合,以描述某些场景专有的信息
对于每一个属性,它的名称都要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示
属性表里几个重要的属性:
Code 属性
Java程序方法体里面的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内。 Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的 方法就不存在Code属性
Exceptions属性
Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Excepitons),也 就是方法描述时在throws关键字后面列举的异常。
LineNumberTable属性
LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它 并不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中使用-g:none或-g:lines 选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行产生的最主要 影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码 行来设置断点。
ConstantValue属性
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类变量)才可以使 用这项属性。类似“int x=123”和“static int x=123”这样的变量定义在Java程序里面是非常常见的事情,但虚拟机对这 两种变量赋值的方式和时刻都有所不同。对非static类型的变量(也就是实例变量)的赋值是在实例构造器<init>() 方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器<clinit>()方法中或者使用ConstantValue属性。 目前Oracle公司实现的Javac编译器的选择是,如果同时使用final和static来修饰一个变量(按照习惯,这里称“常量” 更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就将会生成ConstantValue属性来进行初 始化;如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>()方法中进行初始化。
比如说:
-
public static final String PHONE_NUM= ”123400000“; 如果用static和final 同时修饰string类型的变量,我们在javac编译的过 程 中就把这个PHONE_NUM的值进行了初始化, 放到了constantVaule属性里。 否则,需要在类加载过程的最后一步初始化这 一步调用client方法进行初始化。
如果想知道Class字节码如何一个一个在常量里定位到的,可以借鉴下一下博客
JVM基础系列第5讲:字节码文件结构 - 陈树义 - 博客园 (cnblogs.com)
|