封面图.png

前言

经过上一篇文章《深入理解JVM 上》的内容总结可以让自己知道JVM大致的运行原理,本章接下来介绍的是如何解析与加载类。

Class文件存储的是字节码是为了跨平台运行而设计,这种结构不仅仅是能存储Java语言,比如Kotlin也可以编译为Class文件虚拟机一样可以运行,虚拟机无需关心Class的来源是什么语言因为它更像一个独立的系统。

Class文件结构

Class文件是一组以8个字节为基础单位的二进制流, 各个数据项目严格按照顺序紧凑地排列在文件之中, 中间没有添加任何分隔符, 这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据, 没有空隙存在。 当遇到需要占用8个字节以上空间的数据项时, 则会按照高位在前(高位字节在地址最低位,最低字节在地址最高位来存储数据 )的方式分割成若干个8个字节进行存储。 下方是代码对应编译好的Class文件的HEX,例如:HelloWorld.Class “0xCAFEBABE”四个字节的“魔数” (00~03),接着(04-07)Class主副版本号,这组8个字节是为了保证虚拟机识别文件而设计。

1
2
3
4
5
6
7
8
//此源码使用的是java1.8编译
package cn.error0;
public class HelloWorld {
public static void main(String[] args) {
//Print Hello World
System.out.println("Hello World");
}
}

HelloWorldClassHEX.png

Class文件存储结构

根据《Java虚拟机规范》 的规定, Class文件格式采用一种类似于C语言结构体的伪结构来存储数
据, 这种伪结构中只有两种数据类型: “无符号数”和“表”。 后面的解析都要以这两种数据类型为基
础, 所以需要理解这两个概念。

无符号数属于基本的数据类型, 以u1、 u2、 u4、 u8来分别代表1个字节、 2个字节、 4个字节和8个
字节的无符号数, 无符号数可以用来描述数字、 索引引用、 数量值或者按照UTF-8编码构成字符串
值。
是由多个无符号数或者其他表作为数据项构成的复合数据类型, 为了便于区分, 所有表的命名
都习惯性地以“_info”结尾。 表用于描述有层次关系的复合结构的数据, 整个Class文件本质上也可以视
作是一张表。表是由任意数量的可变长度的项组成,用于表示class文件内容的一系列复合结构。本章出现的ASCII字符都为Unicode码点。

长度 类型(字段 or 表)
u4 magic
u2 minor_version
u2 major_version
u2 constant_ pool_ count
cp_info constant_ pool [constant_ pool_ count- 1]
u2 access_flags
u2 this_class
u2 super_class
u2 interfaces_count
u2 interfaces [interfaces_count]
u2 fields_ count
field_info fields[fields_ count]
u2 methods_count
method_info methods[methods_ count]
u2 attributes_count
attribute_info attributes[attributes_count]

Magic 魔数

Magin的作用是确定这个文件能不能被虚拟机接受,魔数数字固定为0xCAFEBABE不会改变。

minor_version (副版本号) major_version(主版本号)

如果某个class文件的主版本号为M,副版本号为m那个么这个class文件的格式版本号为M.m。

版本号可以按字母顺序排序,比如1.5<2.0<2.1。假设一个class文件版本号为V,那么Mi.0<=V<=MJ.m时才会被次虚拟机支持。

constant_ pool_ count 常量池计数器

constant_ pool_ count 的值等于常量池表中的成员数加1,对于long和double类型有例外情况,因为long和double是占用8个字节所以需要常量池池的成员加2。常量池表的索引值
只有在大于0且小于constant_ pool_ count 时才会认为是有效的。

constant_ pool[] 常量池

常量池tag值不连续是因为解决兼容等问题 具体参考 传送门

constant_ pool[]是一种表结构,它包含class文件结构及其子结构中引用的所以字符串常量、类或接口名、字段名、其他常量。引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同的特征一一 第1个字节作为类型标记,用于确定该项的格式,这个字节称为tag byte (标记字节、标签字节)。

常量池以1 ~ constant_ pool_ count-1 为索引,常量池各个表也有着完全独立的数据结构,他们之前并没有共性和联系。

类型 tag 描述
CONSTANT_ UTF-8_info 1 UTF-8编码的字符串
CONSTANT_ Integer_info 3 整型字面量
CONSTANT_ Float_info 4 浮点型字面量
CONSTANT_ Long_info 5 长整型字面量
CONSTANT_ Double_info 6 双精度浮点型字面量
CONSTANT_ Class_info 7 类或接口的引用
CONSTANT_ String_info 8 字符串类型字面量
CONSTANT_ Fieldref_info 9 字段的符号引用
CONSTANT_ Methodref_info 10 类中的方法引用
CONSTANT_ InterfaceMethodref_info 11 接口中的方法引用
CONSTANT_ NameAndType_info 12 字段或方法的部分符号引用
CONSTANT_ MethodHandle_info 15 表示方法句柄
CONSTANT_ MethodType_info 16 表示方法类型

CONSTANT_ Utf8_info

UTF-8缩略编码与普通UTF-8编码的区别是:
从’\u0001’到’\u007f’之间的字符(相当于1~127的ASCII码) 的缩略编码使用一个字节表示,
从’\u0080’到’\u07ff’之间的所有字符的缩略编码用两个字节表示, 从’\u0800’开始到’\uffff’之间的所有字符
的缩略编码就按照普通UTF-8编码规则使用三个字节表示。

1
2
3
4
5
6
7
/*需要注意的是CONSTANT_Utf8_info用于描述字段、方法等的名称,所以CONSTANT_Utf8_info长度是它们最大的长度 u2最大值为65535,所以超出64kb的变量名或方法名是无法编译的*/
CONSTANT_Utf8_info
{
u1 tag; //{tag:1}
u2 lenth; //length值说明了这个UTF-8编码的字符串长度是多少字节
u1 bytes[length];//长度为length字节的连续数据是一个使用UTF-8缩略编码表示的字符串
}

CONSTANT_ Integer_info

1
2
3
4
5
CONSTANT_Integer_info
{
u1 tag; //{tag:3}
u4 bytes;//按照高位在前存储的int值
}

CONSTANT_ Float_info

1
2
3
4
5
CONSTANT_Float_info
{
u1 tag; //{tag:4}
u4 bytes;//按照高位在前存储的Float值
}

CONSTANT_ Long_info

1
2
3
4
5
CONSTANT_Long_info
{
u1 tag; //{tag:5}
u8 bytes;//按照高位在前存储的Long值
}

CONSTANT_ Double_info

1
2
3
4
5
CONSTANT_Double_info
{
u1 tag; //{tag:6}
u8 bytes;//按照高位在前存储的Double值
}

CONSTANT_ Class_info

1
2
3
4
5
CONSTANT_ Class_info
{
u1 tag; //{tag:7}
u2 name_index;//常量池的索引值,它指向常量池中一个CONSTANT_ Utf8_info类型的常量 代表了这个类或接口的全限定名。
}

CONSTANT_ String_info

1
2
3
4
5
CONSTANT_ String_info
{
u1 tag; //{tag:8}
u2 index;//指向字符串字而量的索引
}

CONSTANT_ Fieldref_info

1
2
3
4
5
6
CONSTANT_ Fieldref_info
{
u1 tag; //{tag:9}
u2 index;//指向声明字段的类或者接口描述符CONSTANT_ Class_info的索引项
u2 index;//指向字段描述符CONSTANT_NameAndType的索引项
}

CONSTANT_ Methodref_info

1
2
3
4
5
6
CONSTANT_ Methodref_info
{
u1 tag; //{tag:10}
u2 index;//指向声明方法的类描述符CONSTANT_Class_info的索引项
u2 index;//指向字段描述符CONSTANT_NameAndType的索引项
}

CONSTANT_ InterfaceMethodref_info

1
2
3
4
5
6
CONSTANT_ InterfaceMethodref_info
{
u1 tag; //{tag:11}
u2 index;//指向声明方法的类描述符CONSTANT_Class_info的索引项
u2 index;//指向字段描述符CONSTANT_NameAndType的索引项
}

CONSTAT_ NameAndType_info

1
2
3
4
5
6
CONSTANT_ NameAndType_info
{
u1 tag; //{tag:12}
u2 index;//指向该字段或方法名称常量项的索引
u2 index;//指向该字段或方法描述符常量项的索引
}

CONSTANT_ MethodHandle_info

1
2
3
4
5
6
CONSTANT_ MethodHandle_info
{
u1 tag; //{tag:15}
u1 reference_ kind //值必须在1至9之间(包括1和9).它决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为
u2 refercnce_index;//值必须是对常量池的有效索引
}

CONSTANT_ MethodType_info

1
2
3
4
5
6
CONSTANT_ MethodType_info
{
u1 tag; //{tag:16}
u2 descriptor_index;//值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Uif8_ info结构,表示方法的描述符

}

access_flags 访问标志

标志名 含义
ACC_PUBLIC 0X0001 声明为public,可以从包外访问
ACC_FINAL 0X0010 声明为final,不允许有子类
ACC_SUPER 0X0020 当用到invokespecial指令时,需要对父类方法做特殊处理
ACC_INTERFACE 0X0200 该class文件定义的是接口而不是类
ACC_ABSTRACT 0X0400 声明为abstract,不能被实例化
ACC_SYNTHETIC 0X1000 声明为synthetic,表示该class文件并非由Java源代码所生成
ACC_ANNOTATION 0X2000 标识注解类型
ACC_ENUM 0X4000 标识枚举类型

this_class 类索引

this_ class 的值必须是对常量池表中某项的一个有效索引值。常量池在这个索引处的成员必须为CONSTANT_Class_ info 类型结构体,该结构体表示这个class文件所定义的类或接口。

super_class 父类索引

对于类来说,super_ class 的值要么是0,要么是对常量池表中某项的一个有效索引值。如果它的值不为0,那么常池在这个索引处的成员必须为CONSTANT_Class_ info 类型常量。如果没有父类默认为0,表示Object类,因为唯一这个类没有父类。

interfaces_count 接口计算器

表示当前类或接口的直接超接口数量。

interfaces[] 接口表

interfaces []中每个成员的值必须是对常量池表中某项的有效索引值,它的长度为interfaces_ count。 每个成员interfaces[i]必须为CONSTANT_ Class_info结构其中0≤interfaces_count 。在interfaces[]中,各成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即interfaces [0]对应的是源代码中最左边的接口。

类索引、父类索引和接口表确定了该类的继承关系,类索引确定了这个类的全限定名,父类索引确定了这个类的父类全限定名。

类索引查找全限定名过程

通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串

类索引查找全限定名过程.png

fields_count 字段计数器

用于表示该类或接口的类字段或者实例字段的数量。

fields[] 字段表

1
2
3
4
5
6
7
8
9
field_info
{
u2 access_flags; //字段的访问权限标志
u2 name_index; //值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Uif8_ info结构,表示方法的描述符
u2 descriptor_index;//值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Uif8_ info结构,表示方法的描述符
u2 attributes_cout;//表示当前字段的附加属性的数量
attribute_info attributes[attributes_cout];//属性表,其值必须是attribute_info

}

字段表的属性和访问各个标志

标志名称 描述
ACC_PUBLIC 0x0001 声明为public,可以从包外访问
ACC_PRIVATE 0x0002 声明为private,只能在定义该字段的类中访问
ACC_PROTECTED 0x0004 声明为protected,子类可以访问
ACC_STATIC 0x0008 声明为static
ACC_FINAL 0x0010 声明为final,对象构造好之后,就不能直接设置该字段了
ACC_VOLATILE 0x0040 声明为volatile,被标识的字段无法缓存
ACC_TRANSIENT 0x0080 声明为transient,被标识的字段不会为持久化对象管理器所写人或读取
ACC_SYNTHETIC 0x1000 被表示的字段由编译器产生,而没有写源代码中
ACC_ENUM 0x4001 该字段声明为某个枚举类型(enum)的成员

class文件中的字段可以设置多个标志。不过有些标志是互斥的一个字段最多只能设置ACC_ PRIVATE, ACC PROTECTED 和ACC_PUBLIC 个标志中的一个,也不能同时设置标志ACC_ FINAL 和ACC VO
接口中的所有字段都具有ACC
PUBLIC、 ACC_ STATIC 和ACC_ FINAL标志,也可以设置ACC_ SYNTHETIC标志,但是不能含有其他标志如果字段带有ACC_ SYNTHETIC 标志,则说明这个字段不是由源码产生的,而是由编译器自动产生的。如果字段带有ACC_ ENUM 标志,这说明这个字段是一个枚举类型的成员。

methods_count 方法计数器

methods_count的值表示当前class文件methods表的成员个数。methods
表中每个成员都是一个 method_ info结构。

methods[] 方法表

methods表中的每个成员都必须是一个method_ info 结构,用于表示当前类或接口中某个方法的完整描述。
method_ info结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、实例初始化方法和类或接口初始化方法。methods表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。

1
2
3
4
5
6
7
8
9
methods_info
{
u2 access_flags; //方法的访问权限标志
u2 name_index; //值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Uif8_ info结构,表示方法的描述符
u2 descriptor_index;//值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Uif8_ info结构,表示方法的描述符
u2 attributes_cout;//表示当前方法的附加属性的数量
attribute_info attributes[attributes_cout];//属性表,其值必须是attribute_info

}

attributes_count 属性计数器

attributes_ count 的值表示当前class文件属性表的成员个数。属性表中每一
项都是一个attribute_ info结构

attributes_info 属性表

1
2
3
4
5
6
methods_info
{
u2 attribute_name_index; //对于任意属性,attribute_name_index必须是对当前class文件的常量池的有效16位无符号索引。常量池在该索引处的成员必须是CONSTANT_ Utf8_ info
u4 attribute_length;
u1 info[attribute_length];
}

实践

本文章只给出了Class结构的部分属性,如需要请参考《Java虚拟机规范》。

环境

1
2
3
4
OS:Mac OS
编译器:VS Code、IDEA
插件:VS Code-hexdump
Java:1.7

代码

1
2
3
4
5
6
7
//此源码使用的是JDK7编译
public class HelloWorld {
public static void main(String[] args) {
//Print Hello World
System.out.println("Hello World");
}
}

HelloWorld.png

IDEA编译完成后class文件存放在/demo/out/

Hex

使用VSCode安装hexdump插件,然后打开Class文件。

VSCodeHex.png

反编译

用VSCode打开Class文件后可以看到前4个字节的十六进制表示的为0xCAFFBABE用于作为JVM可鉴别的文件,接下来四个字节为00 00 00 33 主版本号为33 十进制为51对应的版本号也就是JDK7。

Hex07.png

JDk版本号.png

紧接着为常量池的入口,两个字节(u2)00 22表示为常量池计数器(constant_ pool_ count )的数值 0x0022十进制为34代表这个Class文件的常量池有34项常量值,这个容器计数是从1而不是0开始索引取值为1~33。设计者将第0项常量用于表达不需要引用一个常量池项目含义。除了常量池索引为1开始其他集合都从0开始。

Hex0809.png

第一个常量为(偏移地址0x0000000A~0x0000000E)0A 00 06 00 14,(偏移地址0x0000000A)0x0A十进制为10(tag)对应常量池表为 CONSTANT_ Methodref_info 方法引用 ,接下来(偏移地址0x0000000C)0x0006十进制为6 指向的是第6个常量索引,第6个常量必须为CONSTANT_ Class_info也就是那个类引用这个方法的意思。(偏移地址0x0000000E)两个字节为0x0014十进制20,指向类型为CONSTANT_ NameAndType_info用于描述方法的方法名与返回值类型。可以推断这是一个类方法。

1
2
3
4
5
6
CONSTANT_ Methodref_info
{
u1 tag; //{tag:10}
u2 index;//{index:第6项常量} #27 java/lang/Object
u2 index;//{index:第20项常量} "<init>":()V
}

第二个常量(偏移地址0x0000000F~0x00000103)(偏移地址0x0000000F)0x09十进制为9(tag)对应的常量表为CONSTANT_Fieldref_info,(偏移地址0x00000101)0x0015十进制为21 ,(偏移地址0x00000103)0x0016十进制为22

1
2
3
4
5
6
CONSTANT_Fieldref_info
{
u1 tag; //{tag:9}
u2 index;//{index:第21项常量}
u2 index;//{index:第22项常量}
}

小结

因常量数量太多就不接着手动反编译了(主要怕文章太长了),java官方内置反编译工具 javap可以用来参考或者验证自己手动反编译结果。常量池结束后为代码块存储在常量池的属性表集合用于存储方法执行指令、局部变量等。

使用方式:javap -verbose xxx.class

#1代表为第一个常量,#1 = Methodref 表示这个常量为类中的方法引用,接着为常量的构造值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
Compiled from "HelloWorld.java"
public class HelloWorld
SourceFile: "HelloWorld.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // Hello World
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 LHelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 Hello World
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public HelloWorld();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
ine 2: 0
LocalVariableTable:
Start Length Slot Name Signature 0 5 0 this LHelloWorld;
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}

JVM加载类

类的生命周期

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载
(Loading)、验证(Verification) 、准备(Preparation) 、解析(Resolution) 、初始化
(Initialization)、使用(Using) 和卸载(Unloading) 七个阶段,其中验证、准备、解析三个部分统称
为连接(Linking) 。

加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按 照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始, 这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。请注意,这里笔者写的是 按部就班地“开始”,而不是按部就班地“进行”或按部就班地“完成”,强调这点是因为这些阶段通常都 是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段。

类的生命周期.png

加载

加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对 象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,这里既 可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理), 也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。

验证

这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

准备

准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使 用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为: public static int v = 8080;

实际上变量 v 在准备阶段过后的初始值为 0 而不是 8080,将 v 赋值为 8080 的 put static 指令是 程序被编译后,存放于类构造器方法之中。

但是注意如果声明为:public static final int v = 8080;在编译阶段会为 v 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 v 赋值为 8080。

解析

解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中的:

  1. CONSTANT_Class_info
  2. CONSTANT_Field_info
  3. CONSTANT_Method_info 等类常量

类加载器

虚拟机设计团队把加载动作放到 JVM 外部实现,以便让应用程序决定如何获取所需的类,JVM 提供了 3 种类加载器:

启动类加载器(Bootstrap ClassLoader)负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被 虚拟机认可(按文件名识别,如 rt.jar)的类。

扩展类加载器**(Extension ClassLoader)**

应用程序类加载器(Application ClassLoader)负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。

应用程序类加载器(Application ClassLoader)负责加载用户路径(classpath)上的类库。JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader实现自定义的类加载器。

双亲委派

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。