机器码:CPU可以直接运行的指令。
字节码:二进制代码文件,需要由直译器转义后变成机器码。
6.2 无关性的基石
Java虚拟机的无关性包括平台无关性(一次编写,到处运行)和语言无关性(支持其他语言运行在Java虚拟机上)。Java规范包括了《Java虚拟机规范》和《Java语言规范》,可见从设计之初,Java虚拟机就已经打算和Java语言解耦。Java虚拟机执行Class文件,而各种语言写的代码,经由对应编译器编译成Class文件后,都可以交由java虚拟机执行。
6.3 Class类文件的结构
任何一个Class文件都对应着唯一一个类或者接口的定义信息。Class文件是一组以8个字节为基础单位的二进制流,当需要8字节以上空间的数据项时,会按照高位在前的方式分割成若干个8个字节存储。Class文件中有两种数据类型:“无符号数”、“表”。
- 无符号数属于基本的数据类型,以u1、u2、u4、u8表示1个字节、2个字节、4个字节、8个字节的无符号数。可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型。
6.3.1 魔数与Class文件的版本
每个class文件前4个字节称为魔数,仅用于确定该文件是否是能被虚拟机接受的Class文件。第5、6个字节是次版本号,第7、8个字节是主版本号,高版本的JDK向下兼容前版本的Class文件,但不能运行之后版本的Class文件即使文件格式未发生变化。
6.3.2 常量池
紧接着版本号之后的是常量池,是占用Class文件空间最大的数据项目之一。常量池入口有一个u2类型的数字表示常量的数目,常量池主要用来存放字面量和符号引用。
- 字面量:文本字符串、申明为final的常量值。
- 符号引用:只是一个标记,在虚拟机加载Class文件时解析,翻译成具体的内存地址(动态链接)。
- 被模块导出或者开放的包
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 方法句柄和方法类型
- 动态调用点和动态常量
全限定名:将类全名中的 “.” 替换成“/”。
简单名称:单纯的方法名或者字段名。
描述符:用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。唯一确定一个方法或者字段。
常量池中的每一项常量都是一个表,表的起始位是一个u1类型的标志位,代表当前常量属于哪种常量类型。
6.3.3 访问标志
在常量池之后出现的两个字节表示访问标志,用于识别一些类或者接口层次的访问信息。
6.3.4 类索引、父类索引和接口索引集合
类索引用于确定该类的全限定名,父类索引用于确定父类的全限定名,因为java.lang.Object是所有类的默认继承,所以除了java.lang.Object,其余类的父类索引都不为空。接口索引集合则用于确定该类实现的接口的全限定名。
根据索引找到常量池中CONSTANT_Class_info的类描述符常量,再通过记录的值(也是一个索引)找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。
6.3.5 字段表集合
用于描述接口或者类中声明的变量。包括类变量(static修饰)和实例变量,不包括局部变量。
access_flags:字段的访问标识
name_index和descriptor_index,代表了字段的简单名称以及字段和方法的描述符。如果字段声明时赋值,那么会有额外的属性指向常量。字段表集合中不会列出从父类或者父类接口中继承的字段,但是会出现原本代码中不存在的字段,譬如内部类保持对外部类的访问,会有一个指向外部类实例的字段。
6.3.6 方法表集合
跟字段表类似,不包括方法中的代码,其存放在属性表集合中的一个名为Code的属性中。
如果父类的方法在子类没有被重写(Override),则子类的方法表中不会出现父类的方法信息。会出现类中没有定义的方法,譬如类构造器“<clinit>()”方法和实例构造器“<init>()”方法。
在Java语言中,特征签名=方法名+入参类型+入参顺序
在字节码中, 特征签名=方法名+入参类型+入参顺序+返回值+受查异常表
6.3.7 属性表集合
Class文件、字段表、方法表都可以携带自己的属性表集合。对于每一个属性,它的名称都要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示。
1. Code属性
Java方法体中的代码经过Java编译器处理之后,转变成字节码,存放在Code属性中。
max_stack:代表了操作数栈最大深度,虚拟机运行时需要根据这个值来分配栈帧中的操作栈深度。
max_local:代表了局部变量表所需要的存储空间,存储单位是变量槽(Slot,虚拟机为局部变量分配内存使用的最小单位)。方法参数包括方法中的隐式参数this、catch块中定义的异常、方法体中定义的局部变量,都需要局部变量表存储,分配局部变量表时存在内存复用。
对this的访问实际就是对一个指向当前对象实例的局部变量的访问,该局部变量存在于局部变量表预留出来的第一个变量槽中。
code_length:代表了字节码的长度。理论上可以达到2的32次幂,但是《Java虚拟机规范》限制了一个方法不允许超过65535条字节码指令,所以实际上只用到了u2的长度,如果超过了限制,Java编译器会拒绝编译。
code:用于存储字节码指令的一系列字节流,单位大小为一个字节,可以表示256条指令,目前《Java虚拟机规范》已经定义了其中约200条指令的含义。
Code属性用于描述代码,Class文件中的其他数据用于描述元数据。
2. Exception属性
列出方法中可能抛出的受查异常,即throws之后列出的异常。
3. LineNumberTable属性
用于描述Java源码行号与字节码行号之间的对应关系,非必须,如果没有这一项,则在抛出异常时,栈帧中不会显示行号,且再调试程序时,无法按照源码行设置断点。
4. LocalVariableTable和LocalVariableTypeTable属性
用于描述栈帧中局部变量表的变量和Java源码中定义的变量之间的关系,非必须,如果没有这一项,当其他人引用这个方法,所有参数名都将丢失,譬如IDE用arg0等占位符替代原有的参数名。
5. SourceFile及SourceDebugExtension属性
用于记录生成这个Class文件的源码文件名称。当抛出异常时,堆栈会显示异常所属的文件名称。
6. ConstantValue属性
通知虚拟机自动为静态变量赋值,只有被static修饰的变量才能使用这项属性,属性的值是一个指向常量池的索引。
java中变量分为类变量和实例变量和局部变量。其中实例变量在实例构造器<init>()方法中赋值;对于类变量,赋值方式有两种,一在类构造器的<clinit>()方法中赋值、二在ConstantValue中赋值;
7. InnerClasses属性
用于记录内部类和宿主类之间的关联。如果一个类中定义了内部类,那么编译器将会为它所包含的内部类生成InnerClasses属性。
8. Deprecated及Synthetic属性
Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不推荐使用,在程序中通过“@deprecated”注解进行设置。
Synthetic属性代表此字段或则和方法不是由Java源码直接产生,而是通过编译器自行添加的。编译器通过生成一些源码中不存在的Synthetic方法或者字段甚至整个类,实现越权访问(越过private修饰符)或其他绕开语言限制的功能。唯一例外的是实例构造器”<init>()”方法和类构造器“<clinit>()”。
9. StackMapTable属性
在虚拟机类加载的字节码验证阶段被新类型检查验证器使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
10. Signature属性
可以出现在类、字段表和方法表结构的属性表中。任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,Signature属性会记录下泛型签名信息,在反射时可以通过该属性获取到泛型类型而不是Object。
11. MethodParameters属性
用于记录方法的各个形参名称和信息。LocalVariableTable属性虽然也会记录方法的形参名称,但是这个属性属于Code属性的子属性,抽象方法和接口并没有Code属性。在JDK8中新增了这个属性,用于统一和完整的保存。
6.4 字节码指令简介
Java虚拟机的指令由一个字节长度,代表各种特定操作的数字(称为操作码)以及紧随其后的零至多个代表此操作所需要的的参数(称为操作数)构成。
参考文献:
Java Class文件格式、常量池项目的类型、表的结构(https://blog.csdn.net/m0_37701628/article/details/86684589)