通过《编译原理》的相关学习,我们知道我们编写的Java代码最终会被翻译成class文件。Class文件格式是JVM自己定义的用于表示Java类的二进制字节流规范,与操作系统本身无关,该文件格式正是Java代码一次编译,跨平台运行的关键。
class文件
image.png
其中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方法。
image.png
常量池
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文件和字节码解析》
解析流程
以下面这个例子来说明解析过程
image.png
字节码文件解析流程图,从上到小解析就好了
其实解析字节码就想摩斯密码一样,有一个对应的密码本。对照着就可以解析了。具体可以看书《JVM虚拟机规范》
u1的意思是: 占1个字节 (16进制的一个字节占两位00)
u2的意思是: 占2个字节
u4的意思是: 占4个字节
!的意思是:不确定,是动态计算的
image.png
开始解析
首先说下:上面的字节码都是16进制的,需要计算成10进制的,其次可以看到分成三部分,左边address部分和右边dump部分是没有用的,我们直接解析中间的。还有并不是一排表示一个东西。最后我会把我解析的这个类的信息贴出来。
魔数
ca fe ba be:魔术,表示是一个合格的class文件
为什么会有魔数?魔数有什么用?
其实就是JVM判断是不是一个合格的class文件,如果是就继续往下面读
次版本号和主版本号是指java版本的版本号,看图
00 00:次版本号。这个没啥好解释的把
00 34:主版本号 (3*15+4=52)= jdk1.8
image.png
解析常量池
constant_pool_count是常量池大小
00 20 =32。这里需要注意常量池是从1开始的,所以要减一,理论上是32个,实际是31个。具百度说,0是null。看图验证:工具是jclasslib,idea的插件
image.png
常量池解析
常量池选项属性图。 下面有些属性我有解释,但是具体的解释最好还是看虚拟机规范
image.png
第一个常量池
Methodref_info {
u1 tag; 0A=10
u2 class_index; 00 05 指向常量池5
u2 name_and_type_index; 00 1A 它表示当前字段或方法的名字和描述符
}
第二个常量池,后面一样的我就不解释了
Fieldref_info {
u1 tag; 09
u2 class_index; 00 04
u2 name_and_type_index; 00 1B
}
第三个常量池
Fieldref_info {
u1 tag; 09
u2 class_index; 00 04
u2 name_and_type_index; 00 1C
}
image.png
…后面的我就省略了,一样的
access_flags 类修饰符
00 21 = public 和 super
this_class 当前类
00 04 指向常量池,当前类
image.png
super_class
00 05 指向常量池,父类
interfaces_count :实现的接口数量
00 00 :0那么就跳过interfaces[]
fields_count
00 02 表示该类或接口声明的类字段或者实例字段个数,说白了就是你的成员变量有几个
fields_info[
field_info {
u2 access_flags; 00 08 access_flags 项的值是用于定义字段被访问权限和基础属性的掩码标志 请对表
u2 name_index; 00 06 指向常量池
u2 descriptor_index; 00 07 描述符
u2 attributes_count; 00 00
attribute_info attributes[attributes_count]; 就是如果上面哪个count不为0,这里又是一个数组
}
field_info {
u2 access_flags; 00 08
u2 name_index; 00 08
u2 descriptor_index; 00 07
u2 attributes_count; 00 00
attribute_info attributes[attributes_count];
}
methods_count
00 03 =3个方法个数 应该会好奇为啥会是三个哈?我这明明只写了一个。因为我本身main有一个,构造函数有一个,clint也是一个。clint这个方法是动态生成的,如果类中有静态属性或者静态块就会生成这个方法
解析方法,最难的
methods_info[
method_info {
u2 access_flags; 00 01 access_flags 项的值是用于定义当前方法的访问权限和基本属性的掩码标志
u2 name_index; 00 09 指向常量池
u2 descriptor_index; 00 0A 方法描述符 V代表返回值是void 其他跟字段描述符一样
u2 attributes_count; 00 01 code属性个数
attribute_info attributes[attributes_count];
attributes[
Code_attribute {
u2 attribute_name_index; 00 0B 指向常量池,表示字符串code
u4 attribute_length; 00 00 00 2F attribute_length这个意思是下面所有的字节数加起来是这个数;attribute_length 项的值表示当前属性的长度,不包括开始的 6 个字节
u2 max_stack; 00 01 操作栈数量,是栈帧里面的 在任意时刻,操作数栈都会有一个确定的栈深度,一个 long 或者 double 类型的数据会占用两个单位的栈深度,其他数据类型则会占用一个单位深度
u2 max_locals; 00 01 max_locals 项的值给出了分配在当前方法引用的局部变量表中的局部变量个数
u4 code_length; 00 00 00 05 code_length 项给出了当前方法的 code[]数组的字节数
u1 code[code_length]; 2A B7 00 01 B1 code[]数组给出了实现当前方法的 Java 虚拟机字节码---方法体或者说是字节码指令
u2 exception_table_length; 00 00 异常
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count; 00 02 表示attributes数组的个数
attribute_info attributes[attributes_count];
attributes[
LineNumberTable_attribute { 为什么我知道是这个属性勒?因为00 0C指向常量池12,而12就是LineNumberTable_attribute。
u2 attribute_name_index; 00 0C
u4 attribute_length; 00 00 00 06 attribute_length 给出了当前属性的长度,不包括开始的 6 个字节
u2 line_number_table_length; 00 01 说明line_number_table数组的个数,而line_number_table里的元素就是下面的这个{}
line_number_table[line_number_table_length];
[
{
u2 start_pc; 00 00 code数组的索引
u2 line_number; 00 08 line_number 项的值必须与源文件的行数相匹配
}
]
}
LocalVariableTable_attribute {
u2 attribute_name_index; 00 0D 指向常量池
u4 attribute_length; 00 00 00 0c 当前属性的长度,不包括开始的 6 个字节
u2 local_variable_table_length; 00 01 local_variable_table[]数组的成员个数
local_variable_table[local_variable_table_length];
[
{
u2 start_pc; 00 00 start_pc是local_variable_table的下标
u2 length; 00 05 length两种意思。具体的看书
u2 name_index; 00 0E 指向常量池,表示一个局部变量的有效的非全限定名
u2 descriptor_index; 00 0F 字段描述符
u2 index; 00 00 index为此局部变量在当前栈帧的局部变量表中的索引
}
]
}
]
}
]
}
method_info {
u2 access_flags; 00 09 public static 修饰符 可以相加的
u2 name_index; 00 10
u2 descriptor_index; 00 11
u2 attributes_count; 00 03
attribute_info attributes[attributes_count];
attributes[
Code_attribute {
u2 attribute_name_index; 00 0B 指向常量池,表示字符串code
u4 attribute_length; 00 00 00 2B attribute_length这个意思是下面所有的字节数加起来是这个数;attribute_length 项的值表示当前属性的长度,不包括开始的 6 个字节
u2 max_stack; 00 00 操作栈数量,是栈帧里面的 在任意时刻,操作数栈都会有一个确定的栈深度,一个 long 或者 double 类型的数据会占用两个单位的栈深度,其他数据类型则会占用一个单位深度
u2 max_locals; 00 01 max_locals 项的值给出了分配在当前方法引用的局部变量表中的局部变量个数
u4 code_length; 00 00 00 01 code_length 项给出了当前方法的 下面的code[]数组的字节数
u1 code[code_length]; B1 code[]数组给出了实现当前方法的 Java 虚拟机字节码---方法体或者说是字节码指令
u2 exception_table_length; 00 00 异常
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count; 00 02 表示attributes数组的个数
attribute_info attributes[attributes_count];
attributes[
LineNumberTable_attribute { 为什么我知道是这个属性勒?因为00 0C指向常量池12,而12就是LineNumberTable_attribute。
u2 attribute_name_index; 00 0C
u4 attribute_length; 00 00 00 06 attribute_length 给出了当前属性的长度,不包括开始的 6 个字节
u2 line_number_table_length; 00 01 说明line_number_table数组的个数,而line_number_table里的元素就是下面的这个{}
line_number_table[line_number_table_length];
[
{
u2 start_pc; 00 00 code数组的索引
u2 line_number; 00 10 line_number 项的值必须与源文件的行数相匹配
}
]
}
LocalVariableTable_attribute {
u2 attribute_name_index; 00 0D 指向常量池
u4 attribute_length; 00 00 00 0c 当前属性的长度,不包括开始的 6 个字节
u2 local_variable_table_length; 00 01 local_variable_table[]数组的成员个数
local_variable_table[local_variable_table_length];
[
{
u2 start_pc; 00 00 start_pc是local_variable_table的下标
u2 length; 00 01 length两种意思。具体的看书
u2 name_index; 00 12 指向常量池,表示一个局部变量的有效的非全限定名
u2 descriptor_index; 00 13 字段描述符
u2 index; 00 00 index为此局部变量在当前栈帧的局部变量表中的索引
}
]
}
]
},
Exceptions_attribute {
u2 attribute_name_index; 00 14 指向常量池20,代表字符串exceptions
u4 attribute_length; 00 00 00 04 attribute_length 项的值给出了当前属性的长度,不包括开始的 6 个字节
u2 number_of_exceptions; 00 01 exception_index_table数组成员个数
u2 exception_index_table[number_of_exceptions];
[
Class_info {
u1 tag; 默认等于00 07
u2 name_index; 00 15 指向常量池
}
]
},
{ 这个属性我没找到 methodParamters
00 16 00 00 00 05 01 00 12 00 00属于methodparameters
}
]
}
method_info { 第三个不解析了太累了
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
方法解析完
image.png
attributes_count
00 01 类属性
attributes[attributes_count] = attributes[1]
[
SourceFile_attribute {
u2 attribute_name_index; 00 18
u4 attribute_length; 00 00 00 02
u1 sourcefile_index; 00 19
}
]
一些需要用到对应表,最好看书,详细
字段描述符图
image.png
类和属性访问修饰符图,这个数可以相加,比如0x0009就是public static
image.png
下面是一些解析的时候一些属性集合
field_info
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
method_info
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
attribute_info
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
Code_attribute
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
LineNumberTable_attribute
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{ u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}
LocalVariableTable
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{ u2 start_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
u2 index;
} local_variable_table[local_variable_table_length];
}
SourceFile_attribute
SourceFile_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 sourcefile_index;
}










网友评论