第一章 JVM 概述
第二章 JVM 编译
第三章 JVM 类结构
第四章 JVM 类加载机制
了解 Java 虚拟机类文件结构非常重要,它上承各种 JVM 系编程语言,下接虚拟机。Java 虚拟机完全依赖 class 格式的二进制作为输入,经过类加载器,加载进入元空间的方法区。在虚拟机执行的时候,虚拟机在 JAVA 虚拟机栈中创建栈桢,通过当前栈桢中指向当前方法常量池的引用从方法区获取代码,读取其中的指令,并在栈桢的操作数栈和本地变量表中操作数据和保存结果。
先简单介绍一下 class 文件(并非一定要存储在文件中,可以在类加载器中动态创建)。一个 class 文件仅包含一个类或者接口的定义。每个文件都是8位的字节流,16位、32位、64位的数据可以通过 DataInput 类的 readUnsignedByte()、readUnsignedShort()、readInt()、readLong()来读取。本文用u1、u2、u3、u4来分别表示这四种类型。类文件的格式如下:
ClassFile {
u4 magic; // 值是0xCAFEBABE,标识这个二进制文件是 JVM 类文件格式
u2 minor_version; // 次要版本信息
u2 major_version; // 主版本信息,52 [1.8],50 [1.6],以此类推
u2 constant_pool_count; // 常量池表中常量的个数 + 1,例如本例中#最大是 118,这个值就是 119
cp_info constant_pool[constant_pool_count-1]; // 常量表中的数据,包含字符串常量、类和接口名称,以及其它在这个文件中被引用的常量
u2 access_flags; // 主要用来标识类型(ACC_INTERFACE、ACC_ABSTRACT、ACC_ANNOTATION、ACC_ENUM、ACC_SYNTHETIC)、访问控制(ACC_PUBLIC、ACC_FINAL)
u2 this_class; // 当前类信息,指向有效 CONSTANT_Class_info 结构
u2 super_class; // 父类信息,指向有效 CONSTANT_Class_info 结构
u2 interfaces_count; // 直接父接口数量
u2 interfaces[interfaces_count]; // 父接口数组,每个 interfaces[i] 都应指向有效的 CONSTANT_Class_info 结构
u2 fields_count; // 所有类和实例字段的数量
field_info fields[fields_count]; // 所有类和实例 field_info 结构字段的信息
u2 methods_count; // 所有方法的数量
method_info methods[methods_count]; // 所有 method_info 结构的方法:包含实例方法、类型方法、实例初始化方法、任何类或接口的初始化方法
u2 attributes_count; // 所有属性的数量
attribute_info attributes[attributes_count]; // 所有 attribute_info 结构的属性
}
还是用上一章的代码,仅增加了 implements Serializable,public class JavaCompiler implements Serializable {
,为了丰富字节码的内容。
可以用 sublime 等工具把字节码拖入工具内就可以看到。
通过 IDEA 的 Jclasslib 查看,取值信息可以参考类文件格式中的注释。
描述符是用字符串常量池中Utf8
类型的字符串信息来描述字段和方法类型信息。
字段描述符描述的是字段的类型,包含基本类型、对象类型、数组类型。其格式和取值如下所示。
示例:descriptor: Ljava/lang/Integer;
格式:
FieldDescriptor:
FieldType
FieldType:
BaseType
ObjectType
ArrayType
BaseType:
(one of)B C D F I J S Z
ObjectType:
L ClassName ;
ArrayType:
[ ComponentType
ComponentType:
FieldType
方法描述符包含0或多个参数的类型以及方法的返回类型。参数的类型和字段类型一致,返回类型多了一种 void 类型用 V 表示。
格式:
MethodDescriptor:
( {ParameterDescriptor} ) ReturnDescriptor
ParameterDescriptor:
FieldType
ReturnDescriptor:
FieldType
VoidDescriptor
VoidDescriptor:
V
示例:方法 Object m(int i, double d, Thread t) {...}
的描述符是(IDLjava/lang/Thread;)Ljava/lang/Object;
。
Java 虚拟机指令不依赖于运行时的类、接口、类实例或者数组,而是依赖于常量池中的符号信息。
常量池中的每项信息都可以表示为如下结构:
{
u1: tag, // 1 字节信息表示常量类型,tag 取值见下表
u1: info[] // 2字段以及上长度表示常量内容
}
常量池中的CONSTANT_String、CONSTANT_Integer、CONSTANT_Float、CONSTANT_Long、CONSTANT_Double都很简单就没单独画,把其中最基本的几个画一张图,箭头表示引用下面的结构。
可以借鉴这张图来理解字节码信息。
code 属性在 method_info 结构的属性中,存储方法的指令和一些其它的辅助信息,用户可以通过 code 了解代码的主要内容。
// 代码属性
Code_attribute {
u2 attribute_name_index; // 属性名称索引,指向常量池中的 CONSTANT_Utf8_info
u4 attribute_length; // 属性数据长度
u2 max_stack; // 执行这个方法的时候操作数栈的最大深度
u2 max_locals; // 当执行这个方法的时候最大的本地变量个数,包含传给这个方法的参数
u4 code_length; // 代码数组的长度
u1 code[code_length]; // 代码数据
u2 exception_table_length; // 异常表长度
{ u2 start_pc; // try 代码起始位置,数组下标包含
u2 end_pc; // try 代码结束位置,数组下标不包含
u2 handler_pc; // 异常处理开始的位置,数组下标包含
u2 catch_type; // 需要捕获的异常类型,其或者子类异常会捕获,index 指向 CONSTANT_Class_info
} exception_table[exception_table_length]; // 异常表信息,数组,有序
u2 attributes_count; // 属性的属性数量
attribute_info attributes[attributes_count]; // 属性的属性内容
}
/**
* 调用实例方法, 接收参数
*/
int synchronizedBlockAddTwoInteger (Integer intA, Integer intB) {
synchronized (this) {
return intA + intB;
}
}
int synchronizedBlockAddTwoInteger(java.lang.Integer, java.lang.Integer);
descriptor: (Ljava/lang/Integer;Ljava/lang/Integer;)I
flags:
Code:
stack=2, locals=5, args_size=3
0: aload_0
1: dup
2: astore_3
3: monitorenter
4: aload_1
5: invokevirtual #9 // Method java/lang/Integer.intValue:()I
8: aload_2
9: invokevirtual #9 // Method java/lang/Integer.intValue:()I
12: iadd
13: aload_3
14: monitorexit
15: ireturn
16: astore 4
18: aload_3
19: monitorexit
20: aload 4
22: athrow
Exception table:
from to target type
4 15 16 any
16 20 16 any
LineNumberTable:
line 63: 0
line 64: 4
line 65: 16
LocalVariableTable:
Start Length Slot Name Signature
0 23 0 this Lorg/compiler/JavaCompiler;
0 23 1 intA Ljava/lang/Integer;
0 23 2 intB Ljava/lang/Integer;
StackMapTable: number_of_entries = 1
frame_type = 255 /* full_frame */
offset_delta = 16
locals = [ class org/compiler/JavaCompiler, class java/lang/Integer, class java/lang/Integer, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
表示 code 属性中的代码数组与源码行号的对应关系,可以快速找到字节码对应的源代码信息。
// 表示 code 属性中的代码数组与源码行号的对应关系
LineNumberTable_attribute {
u2 attribute_name_index; // 指向 CONSTANT_Utf8_info
u4 attribute_length; // 属性长度
u2 line_number_table_length; // 数组长度,代表有多个对应关系
{ u2 start_pc; // code 中的数组下标
u2 line_number; // 原始代码的行号
} line_number_table[line_number_table_length]; // 对应关系信息
}
代码属性中的本地变量表主要用来告诉调试器执行期间局部变量的值。
LocalVariableTable_attribute {
u2 attribute_name_index; // 常量池中表示 LocalVariableTable 的索引
u4 attribute_length; // 属性长度
u2 local_variable_table_length; // 局部变量数组长度
{ u2 start_pc; // 当前栈桢开始位置,包含,局部变量可以在这个范围内有值 [start_pc, start_pc + length)
u2 length; // 当前栈桢的结束位置,code 数组长度,不包含
u2 name_index; // 常量池中表示局部变量名称的索引
u2 descriptor_index; // 常量池中表示局部变量类型的索引
u2 index; // 当前桢中局部变量的索引,double/long占用两个索引
} local_variable_table[local_variable_table_length]; // 局部变量信息
}
Java 虚拟机首先确保该文件具有类文件的基本格式,会检查类文件是否完整,magic 数字正确,所有已知属性是否设置正确以及常量池中的信息是否正确。之后会校验代码以及代码的约束,包含静态约束、结构约束以及类文件校验。