通过《编译原理》的相关学习,我们知道我们编写的Java代码最终会被翻译成class文件。Class文件格式是JVM自己定义的用于表示Java类的二进制字节流规范,与操作系统本身无关,该文件格式正是Java代码一次编译,跨平台运行的关键。
class文件
其中u 表示n个无符号字节,如u4 magic 表示magic的取值用4个无符号字节表示,cp_info描述常量池的结构,field_info描述字段的数据结构,method_info描述方法的数据结构,attribute_info描述属性的数据结构。ClassFile结构各项的含义如下:
- magic: 魔数,用于标识当前Class文件的文件格式,JVM可据此判断该文件是否可以被解析,目前固定为0xCAFEBABE
- minor_version, major_version:minor_version是副版本号,major_version是主版本号,这两个版本是生成Class文件时根据编译的JDK版本来确定的,用标识编译时的JDK版本,常见的一个异常Unsupported
major.minor version 52.0就是因为运行时的JDK版本低于编译时的JDK版本,52是Java8的主版本号。 - constant_pool_count:常量池计数器,等于常量池中的成员数加1
- constant_pool:常量池,是一种表结构,包含class文件结构和子结构中引用的所有字符串常量,类或者接口名,字段名和其他常量,其有效索引范围是1- (constant_pool_count-1)。其中类和接口名采用全限定形式,即在整个JVM中的绝对名称,如java.lang.Object,方法名,字段名、局部变量名和形参名都采用非限定名,即在源代码文件中使用相对名称,如属性名name。
- access_flags:用于表示某个类或者接口的访问权限和属性
- this_class:类索引,该值必须是对常量池中某个常量的一个有效索引值,该索引处的成员必须是一个CONSTANT_Class_info类型的结构体,表示这个class文件所定义的类和接口
- super_class:父类索引,同this_class,该值必须是对常量池中CONSTANT_Class_info类型常量的一个有效索引值,如果该值为0,则只能表示java.lang.Object类,因为该类是唯一一个没有父类的类。
- interfaces_count:接口计数器,表示当前类或者接口的直接超接口的数量
- interfaces:接口表,是一个表结构,每个成员同this_class,必须是对常量池中CONSTANT_Class_info类型常量的一个有效索引值,其有效索引范围为0~interfaces_count,接口表中成员的顺序与源代码中给定的接口顺序是一致的,interfaces[0]表示源代码中最左边的接口。
- fields_count:字段计数器,当前class文件所有字段的数量
- fields:字段表,是一个表结构,表中每个成员必须是filed_info数据结构,用于表示当前类或者接口的某个字段的完整描述,不包含从父类或者父接口继承的字段
- methods_count:方法计数器,表示当前类方法表的成员个数
- methods:方法表,是一个表结构,表中每个成员必须是method_info数据结构,用于表示当前类或者接口的某个方法的完整描述,包含当前类或者接口定义的所有方法,如实例方法、类方法、实例初始化方法等,不包含从父类或者父接口继承的方法
- attributes_count:属性计数器,表示当前class文件attributes属性表的成员个数
- attributes:属性表,是一个表结构,表中每个成员必须是attribute_info数据结构,这里的属性是对class文件本身,方法或者字段的补充描述,如SourceFile属性用于表示class文件的源代码文件名。
// 为是一个简单的例子
public class ClassFileTest {
private int a = 5;
public int getA(){
return this.a;
}
public static void main(String[] args) {
new ClassFileTest().getA();
}
}
// javap -v
public class ClassFileTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#24 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#25 // ClassFileTest.a:I
#3 = Class #26 // ClassFileTest
#4 = Methodref #3.#24 // ClassFileTest."<init>":()V
#5 = Methodref #3.#27 // ClassFileTest.getA:()I
#6 = Class #28 // java/lang/Object
#7 = Utf8 a
#8 = Utf8 I
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 LClassFileTest;
#16 = Utf8 getA
#17 = Utf8 ()I
#18 = Utf8 main
#19 = Utf8 ([Ljava/lang/String;)V
#20 = Utf8 args
#21 = Utf8 [Ljava/lang/String;
#22 = Utf8 SourceFile
#23 = Utf8 ClassFileTest.java
#24 = NameAndType #9:#10 // "<init>":()V
#25 = NameAndType #7:#8 // a:I
#26 = Utf8 ClassFileTest
#27 = NameAndType #16:#17 // getA:()I
#28 = Utf8 java/lang/Object
{
public ClassFileTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_5
6: putfield #2 // Field a:I
9: return
LineNumberTable:
line 1: 0
line 3: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this LClassFileTest;
public int getA();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: ireturn
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LClassFileTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: new #3 // class ClassFileTest
3: dup
4: invokespecial #4 // Method "<init>":()V
7: invokevirtual #5 // Method getA:()I
10: pop
11: return
LineNumberTable:
line 10: 0
line 11: 11
LocalVariableTable:
Start Length Slot Name Signature
0 12 0 args [Ljava/lang/String;
}
例如#2对应类型是Fieldref,表示为ClassFileTest.a:I.
例如#5对应类型是Methodref,表示为ClassFileTest.getA:()I.
什么意思?
描述符
描述符有两种,字段描述符和方法描述符,本质就是一个基于特定规则的字符串,其中字段描述符用来表示类,实例和局部变量的类型,具体如下:
ClassFileTest.a:I 表示 ClassFileTest类中int成员变量a.
ClassFileTest.getA:()I.表示 ClassFileTest类中返回值是int的getA方法。
常量池
Java虚拟机指令不依赖类,接口,类实例或数组的运行时内存布局,而是依赖依赖常量池表中的符号信息,常量池表中所有项都有如下通用格式:
cp_info{
u1 tag;//类型标记,用于确定后面的info的格式,tag是一个字节
u1 info[];//两个或者多个字节,取决于tag的值
}
CONSTANT_Utf8_info:用于表示一个Utf8编码的字符串
CONSTANT_Utf8_info{
u1 tag;//tag取值1
u2 length;//后面的byte数组的长度
u1 bytes[length];//字符串对应的byte数组数据
}
CONSTANT_Class_info:用于表示一个Java类或者接口名
CONSTANT_Class_info{
u1 tag;//tag取值7
u2 name_index;//对常量池的有效索引,该索引处的成员必须是一个CONSTANT_Utf8_info结构
}
CONSTANT_Fieldref_info:用于描述一个字段
CONSTANT_Fieldref_info{
u1 tag;//tag取值9
u2 class_index;//常量池中的有效索引,该索引处的成员必须是一个CONSTANT_Class_info结构,表示该字段所属的类
u2 name_and_type_index;//常量池中的有效索引,该索引处的成员必须是一个CONSTANT_NameAndType_info结构,该结构用于表示一个字段或者方法描述符。
}
CONSTANT_MethodType_info:用于记录方法的类型信息,即方法描述符
CONSTANT_MethodType_info{
u1 tag;//tag取值21
u2 name_index;//对常量池的有效索引,该索引处的成员必须是一个CONSTANT_Utf8_info结构
u2 descriptor_index;//对常量池的有效索引,该索引处的成员必须是一个CONSTANT_Utf8_info结构
}
更多类型请参考《Java虚拟机规范8版》
字段
feild_info{
u2 access_flags;//字段标识
u2 name_index;//对常量池的有效索引,该索引处的成员必须是一个CONSTANT_Utf8_info结构
u2 descriptor_index;//对常量池的有效索引,该索引处的成员必须是一个CONSTANT_Utf8_info结构
u2 attributes_count;//当前字段附加属性数值
attribute_info attributes[attributes_count];//属性表中的每个成员
}
方法
method_info{
u2 access_flags;//方法标识
u2 name_index;//对常量池的有效索引,该索引处的成员必须是一个CONSTANT_Utf8_info结构
u2 descriptor_index;//对常量池的有效索引,该索引处的成员必须是一个CONSTANT_Utf8_info结构
u2 attributes_count;//当前字段附加属性数值
attribute_info attributes[attributes_count];//属性表中的每个成员
}
属性
ClassFile、filed_info、method_info结构和Code属性都有属性表,所有的属性都通过attribute_info结构表示,其通用格式如下:
attribute_info{
u2 attribute_name_index;//对常量池的有效索引,该索引处的成员必须是一个CONSTANT_Utf8_info结构
u4 attribute_length;//表示后面的info信息的字节长度
u1 info[attribute_length];//具体数据
}
Java8预定义了23种属性(《Java虚拟机规范8版》中有介绍),例
Code:位于method_info的属性表中,表示该方法的虚拟机指令及辅助信息,method_info中有且仅有一个Code属性,其结构如下:
attribute_info{
u2 attribute_name_index;//对常量池的有效索引,该索引处的成员必须是一个CONSTANT_Utf8_info结构
u4 attribute_length;//表示后面的info信息的字节长度
u2 max_stack;//当前方法操作数栈的最大深度
u2 max_locals;//此方法引用局部变量表中的局部变量的个数,包含传递方法入参的局部变量
u4 code_length;//后面的code数组的字节长度
u1 code[code_length];//当前方法的虚拟机指令的数据
u2 exception_table_length;//后面的exception_table数组的长度;
{
u2 start_pc;
u2 end_pc;//try/catch的代码范围,具体来说是起止代码对应的虚拟机指令在code数组中的索引
u2 handler_pc;//异常处理逻辑的代码的虚拟机指令在code数组中的索引
u2 catch_type;//常量池中一个类型为CONSTANT_Class_info的有效索引,表示捕获的异常类型。
}exception_table[exception_table_length];//此方法的捕获的各异常的异常处理逻辑
}
用户在编译源代码文件时可以添加新的属性,只要JVM实现能够正确识别该属性即可,注意用户自定义的属性不能使用已有预定义属性的属性名
虚拟机指令集
C/C++的方法会被编译成特定于CPU架构的汇编指令,然后交由CPU逐一执行,因为汇编指令与CPU架构是强绑定的,所以C/C++程序在执行前需要在不同CPU架构的机器上编译一遍。Java为了实现一处编译,跨平台运行的目标,在汇编指令之上引入了一个独立于平台的中间层,虚拟机指令,由Java虚拟机规范提供指令标准定义,由Java虚拟机厂商提供指令实现,不同平台的Java虚拟机都遵循相同的指令集规范,从而实现跨平台运行目标。一个方法对应的一组虚拟机指令称为这个方法的字节码(byte codes)。
具体虚拟机指令集请查询《Java虚拟机规范8版》,例:
public int getA();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: ireturn
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LClassFileTest;
操作码 | 助记符 | 指令含义 | 例子操作 |
---|---|---|---|
42 | aload_0 | 将第一个引用类型本地变量推送至栈顶 | this对象放入栈顶 |
180 | getfield | 获取指定类的实例字段,并将值压入栈顶 | 获取a的值 |
172 | ireturn | 从当前方法返回int | 返回a的值 |
主要参考
《hotspot实战》
《Java虚拟机规范8版》
《Hotspot class文件和字节码解析》