四、类加载及字节码技术

4.1、类文件结构

1、类文件结构介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ClassFile {
u4 magic; // 魔数
u2 minor_version; // 小版本号
u2 major_version; // 主版本号
u2 constant_pool_count; // 常量池的数量
cp_info constant_pool[constant_pool_count - 1]; // 常量池
u2 access_flags; // Class 的访问标记
u2 this_class; // 当前类
u2 super_class; // 父类
u2 interfaces_count; // 接口数目
u2 interfaces[interfaces_count]; // 一个类可以实现多个接口
u2 fields[fields_count]; // 类的字段属性集合
field_infou2 methods_count; // 方法个数
method_info methods[methods_count]; // 方法集合
u2 attributes_count; // 这个类的属性表中的属性数
attribute_info attributes[attributes_count]; // 属性表集合
}

2、魔数 magic

0 - 3 字节,表示它是否是【class】类型的文件

class 文件的魔数为:ca fe ba be

3、版本

4 - 7 字节,表示类的版本,小版本没有体现,这里只体现大版本

00 34 表示 Java 8

image-20210523122045814

3、常量池

紧接着主次版本号之后的是常量池,常量池的数量是 constant_pool_count-1(常量池计数器是从1开始计数的,将第0项常量空出来是有特殊考虑的,索引值为0代表“不引用任何一个常量池项”)。

常量池主要存放两大常量:字面量和符号引用。字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。而符号引用则属于编译原理方面的概念。包括下面三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

image-20210523122341807

4.2、字节码指令

1、JavaP

自己分析二进制类文件结构实在太麻烦了,所以 Oracle 提供了 JavaP 工具来反编译 class 文件

  • 使用方法
1
javap -v HelloWorld.class
  • JavaP 反编译一个 HelloWorld 程序,编写一个 HelloWorld ,
1
2
3
4
5
6
package com.hzx.homework;
public class JVMStack {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
  • 查看反编译后的类文件
1
javap -v JVMStack.class
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
68
69
70
71
72
73
74
75
76
77
78
79
Classfile /E:/java/JavaStudy/homework/target/classes/com/hzx/homework/JVMStack.class // 类文件所处位置
Last modified 2021-5-23; size 562 bytes // 最后修改时间和大小
MD5 checksum 38a431810d84ef9d36c4de4964f8d3ca // md5加密签名
Compiled from "JVMStack.java" // class文件对应的Java源文件
public class com.hzx.homework.JVMStack // 全类名和修饰符
minor version: 0
major version: 52 // 版本,52对应JDK8
flags: ACC_PUBLIC, ACC_SUPER // 访问修饰符
Constant pool: // 常量池
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V // 方法引用,引用 Object 类的构造方法
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream; // 属性引用,引用System中的out
#3 = String #23 // Hello World! // 字符串
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V //方法引用,引用println方法
#5 = Class #26 // com/hzx/homework/JVMStack // 引用JVMStack类
#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 Lcom/hzx/homework/JVMStack;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 JVMStack.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 com/hzx/homework/JVMStack
#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
{
// JVMStack类的构造方法
public com.hzx.homework.JVMStack();
descriptor: ()V
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:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/hzx/homework/JVMStack;
// main方法
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
// 加载 PrintStream 中的 System.out属性
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
// 加载常量池的字符串 Hello World!
3: ldc #3 // String Hello World!
// 调用println方法
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 12: 0
line 13: 8
// 局部变量表,这个String变量从第0行开始起作用,作用条数为 9 条,名为 args
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "JVMStack.java"

2、图解方法执行流程

  • 演示字符码指令和操作数栈、常量池的关系
1
2
3
4
5
6
7
8
public class JVMStack {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
  • 使用 Javap 反编译
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
Classfile /E:/java/JavaStudy/homework/target/classes/com/hzx/ho
mework/JVMStack.class
Last modified 2021-5-23; size 620 bytes
MD5 checksum af955ebd4b5aaa456c3b70d8541a8dbb
Compiled from "JVMStack.java"
public class com.hzx.homework.JVMStack
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#25 // java/lang/Object."
<init>":()V
#2 = Class #26 // java/lang/Short
#3 = Integer 32768
#4 = Fieldref #27.#28 // java/lang/System.o
ut:Ljava/io/PrintStream;
#5 = Methodref #29.#30 // java/io/PrintStrea
m.println:(I)V
#6 = Class #31 // com/hzx/homework/J
VMStack
#7 = Class #32 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcom/hzx/homework/JVMStack;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 a
#20 = Utf8 I
#21 = Utf8 b
#22 = Utf8 c
#23 = Utf8 SourceFile
#24 = Utf8 JVMStack.java
#25 = NameAndType #8:#9 // "<init>":()V
#26 = Utf8 java/lang/Short
#27 = Class #33 // java/lang/System
#28 = NameAndType #34:#35 // out:Ljava/io/Print
Stream;
#29 = Class #36 // java/io/PrintStrea
m
#30 = NameAndType #37:#38 // println:(I)V
#31 = Utf8 com/hzx/homework/JVMStack
#32 = Utf8 java/lang/Object
#33 = Utf8 java/lang/System
#34 = Utf8 out
#35 = Utf8 Ljava/io/PrintStream;
#36 = Utf8 java/io/PrintStream
#37 = Utf8 println
#38 = Utf8 (I)V
{
public com.hzx.homework.JVMStack();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/la
ng/Object."<init>":()V
4: return
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/hzx/homework/JVMStack;


public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 12: 0
line 13: 3
line 14: 6
line 15: 10
line 16: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
3 15 1 a I
6 12 2 b I
10 8 3 c I
}
SourceFile: "JVMStack.java"
  • 常量池载入运行时常量池

image-20210523134620444

  • 方法字节码载入方法区

image-20210523134810937

  • main 线程开始运行,分配栈帧内存

绿色部分为局部变量表,蓝色部分时栈深度

image-20210523135132136

  • 在执行 int c = a + b; 时,会将局部变量表中的a和b读取到操作数栈中
  1. 读取 a = 10 到操作数栈中

image-20210523135557994

  1. 将 b = 32768 (Short.MAX_VALUE + 1) 读取到操作数栈中

image-20210523135849999

  1. 此时执行引擎会执行一个iadd 指令,进行加法运算

弹出操作数栈的两个数进行运算,然后将结果压入操作数栈中

image-20210523135924433

  1. 将结果赋值给 c

image-20210523140033286

3、请从字节码角度分析以下代码

1
2
3
4
5
6
7
8
9
10
11
public class JVMStack {
public static void main(String[] args) {
int i = 0;
int x = 0;
while (i < 10) {
x = x++;
i++;
}
System.out.println(x);
}
}
  • 首先执行 int x = 0 ,将局部变量表中的 x 赋值为0

image-20210523173847036

  • x++ 发生在局部变量表的插槽中
  1. 由于执行的是 x++,所以先执行 iload_x ,然后执行 iinc x 1
  2. 执行 iload_x 时,将局部变量表中的 x 的值读入操作数栈,即操作数栈的 x = 0.

image-20210523174324099

  1. 执行 iinc x 1 将局部变量表中的 x 的值 +1,此时值为1

image-20210523174651641

  1. 执行赋值操作,将操作数栈中的 x 的值(0)覆盖掉局部变量表中 x 的值(1)

image-20210523174813082

  • 所以在执行十次循环后,打印的结果仍然是 0 ,虽然真的做了自增操作,但是由于使用操作数栈的值覆盖了局部变量表的值,所以自增失效

image-20210523175213764

4、构造方法 – <cinit>()v

相当于类的构造方法,在类被加载时调用

  • 分析以下代码
1
2
3
4
5
6
7
8
9
10
11
public class JVMStack {
static int i = 10;

static {
i = 20;
}

static {
i = 30;
}
}

Java中,所有静态变量的赋值操作、静态代码块的代码都会被合并为一个方法。

编译器会按照从上到下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 <cinit>()v,这个方法没有参数,也没有返回值

1
2
3
4
5
6
7
 0: bipush        10
2: putstatic #2 // Field i:I
5: bipush 20
7: putstatic #2 // Field i:I
10: bipush 30
12: putstatic #2 // Field i:I
15: return

可以看到,编译器会将静态代码块和静态变量赋值的所有代码收集起来形成一个新方法,在上面的代码中,i 被赋值了三次,其中以最后一次赋值为主。

<cinit>()v 方法会在类加载的初始化阶段被调用

5、构造方法 – <init>()v

对象的构造方法,在创建每一个对象时被调用

  • 分析以下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class JVMStack {
private String a = "s1";

{
b = 20;
}

private int b = 10;

{
a = "s2";
}

public JVMStack(String a,int b) {
this.a = a;
this.b = b;
}

public static void main(String[] args) {
JVMStack stack = new JVMStack("s3",20);
System.out.println(stack.a);
System.out.println(stack.b);
}
}
  • 反编译 JVMStack.class

可以看到,编译器同样会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始的构造方法内的代码总是被放在最后

  • 所以可以得到创建一个新对象时的赋值顺序为
  1. 运行 private String a = "s1"; ,此时对象的成员变量 a 值为 s1
  2. 运行 非静态代码块 {b = 20;},此时对象的成员变量 b 值为 20
  3. 执行 private int b = 10; ,覆盖成员b的值
  4. 执行 {a = s2;},覆盖成员变量 a 的值
  5. 最后执行构造方法,再次覆盖成员变量的值
  6. 结果

image-20210523192400878

  • 反编译后可以看到,此时生成了一个新的构造方法,虽然参数还是两个,但字节码中构造方法和代码和我们定义的代码有区别
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
  public com.hzx.homework.JVMStack(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: invokespecial #1 // Method java/la
ng/Object."<init>":()V
4: aload_0
5: ldc #2 // String s1
7: putfield #3 // Field a:Ljava/
lang/String;
10: aload_0
11: bipush 20
13: putfield #4 // Field b:I
16: aload_0
17: bipush 10
19: putfield #4 // Field b:I
22: aload_0
23: ldc #5 // String s2
25: putfield #3 // Field a:Ljava/
lang/String;
28: aload_0
29: aload_1
30: putfield #3 // Field a:Ljava/
lang/String;
33: aload_0
34: iload_2
35: putfield #4 // Field b:I
38: return

6、方法调用

  • 看一下几种不同的方法调用的字节码指令是否一样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class JVMStack {
public JVMStack() {}
private void test1() {}

private final void test2() {}

public void test3() {}

public static void test4() {}

public static void main(String[] args) {
JVMStack stack = new JVMStack();
stack.test1();
stack.test2();
stack.test3();
JVMStack.test4();
}
}

反编译后结果

  1. 如果是构造方法,那么调用指令是 invokespecial
  2. 如果调用的方法是私有方法(不管有没有使用 final 修饰),那么调用指令是 invokespecial
  3. 如果调用的方法是公共方法(不管有没有使用 final 修饰),那么调用指令是 invokevirtual
  4. 如果调用的方法是静态方法(不管使用什么访问修饰符,是否有 final 修饰),那么调用指令是 invokestatic

对于静态方法、构造方法和私有方法,在编译期间就可以直到他具体要调用哪个类的哪个方法,但公共方法无法知晓(多态)要调用哪个对象的哪个方法

invokevirtual 方法在运行时才能知道需要调用哪个具体方法,这称为动态绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/hzx/homework/JVMStack
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #4 // Method test1:()V
12: aload_1
13: invokespecial #5 // Method test2:()V
16: aload_1
17: invokevirtual #6 // Method test3:()V
20: invokestatic #7 // Method test4:()V
23: return

7、多态原理

在上面我们知道,在调用对象的 public 成员方法时,使用的字节码指令为 invokevirtual

  • 创建一个抽象类 - Animal,然后让两个类(猫和狗)继承这个抽象类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
abstract class Animal {
public abstract void eat();

@Override
public String toString() {
return "我是" + this.getClass().getSimpleName();
}
}

class Dog extends Animal {

@Override
public void eat() {
System.out.println("啃骨头...");
}
}

class Cat extends Animal {

@Override
public void eat() {
System.out.println("吃鱼...");
}
}
  • 在类中使用多态
1
2
3
4
5
6
7
8
9
10
11
public class JVMStack {
public static void test(Animal animal) {
animal.eat();
System.out.println(animal.toString());
}

public static void main(String[] args) {
test(new Cat());
test(new Dog());
}
}
  • 多态动态绑定过程

当执行 invokevirtual 指令时

  1. 先通过栈帧中的对象引用找到对象
  2. 分析对象头,找到对象的实际 Class
  3. Class 结构中有 vtable(虚方法表),它在类加载的链接阶段就已经根据方法的重写规则生成好了
  4. 查表得到方法的具体地址
  5. 执行方法的字节码

8、异常处理

  • try-catch
  1. 使用 javap 反编译以下代码
1
2
3
4
5
6
7
8
9
10
public class JVMStack {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
}
}
}
  1. 查看反编译后的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 12
8: astore_2
9: bipush 20
11: istore_1
12: return
Exception table:
from to target type
2 5 8 Class java/lang/Exception

8,9,11 行的代码是 catch 块的内容,如果没有出错,那么在执行完 1,2,4 代码后会直接 goto(跳转) 到 12 行

Exception table 中定义了 try 块的代码范围,即如果 2 - 5 行出现异常,那么就会直接转到第 8 行,出现异常的类型为 java/lang/Exception

  • 多个 single-catch 块的情况

反编译以下代码的字节码文件

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (NullPointerException e) {
i = 20;
} catch (IndexOutOfBoundsException e) {
i = 30;
} catch (Exception e) {
i = 40;
}
}

编译后结果

  1. 如果 2 - 5 行的代码出现 NullPointerException 那么跳到第8行
  2. 如果 2 - 5 行的代码出现 IndexOutOfBoundsException 那么跳到第15行
  3. 如果 2 - 5 行的代码出现 其他异常 那么跳到第22行
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
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 26
8: astore_2
9: bipush 20
11: istore_1
12: goto 26
15: astore_2
16: bipush 30
18: istore_1
19: goto 26
22: astore_2
23: bipush 40
25: istore_1
26: return
Exception table:
from to target type
2 5 8 Class java/lang/NullPointerException
2 5 15 Class java/lang/IndexOutOfBoundsException
2 5 22 Class java/lang/Exception
LocalVariableTable:
Start Length Slot Name Signature
9 3 2 e Ljava/lang/NullPointerException;
16 3 2 e Ljava/lang/IndexOutOfBoundsException;
23 3 2 e Ljava/lang/Exception;
0 27 0 args [Ljava/lang/String;
2 25 1 i I

由于在一时间内只能进入异常表(Exception Table)中的一个分支,所以这里实现了槽位的复用,三个异常用的槽位都是同一个 slot 2

image-20210527131350136

  • multi - catch 的情况

反编译以下代码 class 文件

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
try {
Method test = TestJVM.class.getMethod("test");
test.invoke(null);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
public static void test() {
System.out.println("ok!");
}

查看结果

从异常表中可知,被监控的代码范围为 0 - 22 ,此时不管是发生 NoSuchMethodException 异常,IllegalAccessException 异常,还是发生 InvocationTargetException 异常,都会跳转到 25 行

27 行中的指令表示执行异常对象的 printStackTrace 方法

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
  public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: ldc #2 // class com/hzx/decim
al/TestJVM
2: ldc #3 // String test
4: iconst_0
5: anewarray #4 // class java/lang/Cla
ss
8: invokevirtual #5 // Method java/lang/Cl
ass.getMethod:(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/refle
ct/Method;
11: astore_1
12: aload_1
13: aconst_null
14: iconst_0
15: anewarray #6 // class java/lang/Obj
ect
18: invokevirtual #7 // Method java/lang/re
flect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lan
g/Object;
21: pop
22: goto 30
25: astore_1
26: aload_1
27: invokevirtual #11 // Method java/lang/Re
flectiveOperationException.printStackTrace:()V
30: return
Exception table:
from to target type
0 22 25 Class java/lang/NoSuchMethodException
0 22 25 Class java/lang/IllegalAccessException
0 22 25 Class java/lang/reflect/InvocationTarge
tException
LocalVariableTable:
Start Length Slot Name Signature
12 10 1 test Ljava/lang/reflect/Method;
26 4 1 e Ljava/lang/ReflectiveOperationEx
ception;
0 31 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 89 /* same_locals_1_stack_item */
stack = [ class java/lang/ReflectiveOperationException ]
frame_type = 4 /* same */

public static void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #12 // Field java/lang/Sys
tem.out:Ljava/io/PrintStream;
3: ldc #13 // String ok!
5: invokevirtual #14 // Method java/io/Prin
tStream.println:(Ljava/lang/String;)V
8: return
}
  • finally

反编译以下代码的 class 文件

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}

查看结果

finally 块一定会执行的原因,这是由于 finally 中的代码被编译后的指令会被分别放在 try 和各个 catch 块中。

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
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1 // 0 -> i
2: bipush 10 // try ---------------
4: istore_1 // 10 -> i
5: bipush 30 // finally
7: istore_1 // 30 -> i
8: goto 27 // return
11: astore_2 // catch Exception e
12: bipush 20 // 20 -> i
14: istore_1 // finally
15: bipush 30 // 30 -> i
17: istore_1
18: goto 27
21: astore_3
22: bipush 30
24: istore_1
25: aload_3
26: athrow
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any
11 15 21 any
LocalVariableTable:
Start Length Slot Name Signature
12 3 2 e Ljava/lang/Exception;
0 28 0 args [Ljava/lang/String;
2 26 1 i I

9、synchronized

  • 从字节码角度探究 synchronized 原理,我们反编译以下代码的类文件
1
2
3
4
5
6
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok!");
}
}
  • 查看结果
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
  public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #2 // class java/lang/Obj
ect
3: dup
4: invokespecial #1 // Method java/lang/Ob
ject."<init>":()V
7: astore_1 // Object 类的 lock 引用 --> lock
8: aload_1 // <- lock (synchronized开始)
9: dup
10: astore_2 // lock引用 -> slot 2
11: monitorenter // monitorenter(lock 引用)
12: getstatic #3 // Field java/lang/Sys
tem.out:Ljava/io/PrintStream;
15: ldc #4 // String ok!
17: invokevirtual #5 // Method java/io/Println
tStream.println:(Ljava/lang/String;)V
20: aload_2 // slot 2 (lock 引用)
21: monitorexit // monitorexit
22: goto 30
25: astore_3
26: aload_2
27: monitorexit
28: aload_3
29: athrow
30: return
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 lock Ljava/lang/Object;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 25
locals = [ class "[Ljava/lang/String;", class java/lang/Ob
ject, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
}
SourceFile: "TestJVM.java"

  • monitorenter

对 lock 引用指向的对象进行加锁操作

  • monitorexit

对 lock 引用指向的对象进行解锁操作

  • synchronized 一定会释放锁的原因
1
2
3
4
5
6
25: astore_3
26: aload_2
27: monitorexit
28: aload_3
29: athrow
30: return

如果 synchronized 包围的代码块(8 - 23)中出现异常,那么此时会走到 25 行,然后执行 monitorexit 释放锁

4.3、编译期处理

所谓的语法糖,其实就是指java编译器把 .java 源码编译为 .class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是java编译器给我们的一个额外福利(给糖吃嘛)
注意,以下代码的分析,借助了javap工具,idea的反编译功能,idea插件jclasslib等工具。另外,编译器转换的结果直接就是class字节码,只是为了便于阅读,给出了几乎等价的java源码方式,并
不是编译器还会转换出中间的java源码,切记。

1、默认构造器

  • .java 源码
1
2
public class JVMStack {
}
  • 编译成 class 后的代码
1
2
3
4
5
6
7
8
public class JVMStack {
// 会自动生成一个无参构造函数
public JVMStack() {
// 调用父类 Object 的无参构造函数,即调用
// java/lang/Object."<init>":()v
super();
}
}

2、自动拆装箱

这个特性是 JDK 5 后开始加入的,代码片段如下:

1
2
Integer x = 1;
int y = x;

以上片段在 JDK 5 之前无法编译通过,必须改写为以下代码

1
2
Integer x = Integer.valueOf(1);
int y = x.intValue();

显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回切换(尤其是集合类中操作的都是包装类),因此这些转换在 JDK 5 后都由编译器在编译阶段完成,即代码片段1会被转换为代码片段2

3、泛型集合取值

泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:

1
2
3
List<Integer>list=newArrayList<>();
list.add(10);//实际调用的是List.add(Objecte)
Integerx=list.get(0);//实际调用的是Objectobj=List.get(intindex);

所以在取值时,编译器真正生成的字节码中,还需要做一次类型转换操作

1
2
// 需要将 Object 转换为 Integer
Integer x = (Integer)list.get(0);

如果使用基本类型接收,那么还需要进行拆箱

1
int x = ((Integer)list.get(0)).intValue();
  • 反编译以下代码的字节码文件
1
2
3
4
5
6
7
8
9
public class JVMStack {
public static void main(String[] args) {
List<Integer> list= new ArrayList<>();
// 实际调用的是List.add(Object e)
list.add(10);
// 实际调用的是Object obj=List.get(int index);
Integer x=list.get(0);
}
}
  • 查看结果

擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable 仍然保留了方法参数泛型的信息

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
  public com.hzx.homework.JVMStack();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/la
ng/Object."<init>":()V
4: return
LineNumberTable:
line 13: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/hzx/homework/JVMStack;


public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class java/uti
l/ArrayList
3: dup
4: invokespecial #3 // Method java/ut
il/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: bipush 10
11: invokestatic #4 // Method java/la
ng/Integer.valueOf:(I)Ljava/lang/Integer;
14: invokeinterface #5, 2 // InterfaceMetho
d java/util/List.add:(Ljava/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
22: invokeinterface #6, 2 // InterfaceMetho
d java/util/List.get:(I)Ljava/lang/Object;
27: checkcast #7 // class java/lan
g/Integer
30: astore_2
31: return
LineNumberTable:
line 15: 0
line 17: 8
line 19: 20
line 20: 31
LocalVariableTable:
Start Length Slot Name Signature
0 32 0 args [Ljava/lang/String;
8 24 1 list Ljava/util/List;
31 1 2 x Ljava/lang/Integer;
LocalVariableTypeTable:
Start Length Slot Name Signature
8 24 1 list Ljava/util/List<Ljava/lang/
Integer;>;
}
SourceFile: "JVMStack.java"

使用 27:checkcast 指令将 Object 类型转换为 Integer

  • 此时使用反射仍然可以获取这些信息

4、可变参数

可变参数也是 JDK 5 后加入的新特性

  • 反编译以下代码的字节码文件
1
2
3
4
5
6
7
8
9
public class JVMStack {
public static void foo(String ...args) {
String[] array = args;
System.out.println(array);
}
public static void main(String[] args) {
foo("hello","world!");
}
}

可变参数 String … args 实际上是一个 String 数组,从代码的赋值语句中就可以看出。

同样 Java 编译器会在编译期间将上述代码变换为:

  • 查看结果
1
2
3
4
5
6
7
8
9
public class JVMStack {
public static void foo(String[] args) {
String[] array = args;
System.out.println(array);
}
public static void main(String[] args) {
foo("hello","world!");
}
}

注意:如果调用 foo() 时不传入参数时,此时等价代码为 foo(new String[]{}),不会传递一个 null 进去

5、foreach 循环

  • foreach 循环也是 JDK 5 后引入的语法糖,其中数组赋初值的简写也是语法糖,数组循环:
1
2
3
4
5
6
7
8
public class JVMStack {
public static void main(String[] args) {
int[] array = {1,2,3,4,5};
for (int e : array) {
System.out.println(e);
}
}
}

等同于:

1
2
3
4
5
6
7
8
9
10
11
12
public class JVMStack {
public JVMStack() {

}
public static void main(String[] args) {
int[] array = new int[]{1,2,3,4,5};
for (int i = 0; i < array.length; ++i) {
int e = array[i];
System.out.println(e);
}
}
}
  • 如果遍历的不是数组而是 List 集合
1
2
3
4
5
6
7
8
public class JVMStack {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
for (Integer i : list) {
System.out.println(i);
}
}
}

以上代码会被编译器转换为对迭代器的调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class JVMStack {
public JVMStack() {

}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Iterator iter = list.iterator();
while (iter.hasNext()) {
Integer e = (Integer) iter.next();
System.out.println(e);
}
}
}

注意:foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其中 Iterable 用来获取集合的迭代器(Iterator)

6、switch 字符串

  • 从 JDK 7 开始, switch 可以作用于字符串和枚举类,这个功能也是语法糖
1
2
3
4
5
6
7
8
9
10
11
12
public static void choose(String str) {
switch(str) {
case "hello": {
System.out.println("h");
break;
}
case "world": {
System.out.println("w");
break;
}
}
}

注意,当 switch 配合 String 和枚举类使用时,变量不能为 null

  • 以上代码等价于

可以看到,执行了两遍switch,第一遍是根据字符串的 hashCodeequals 将字符串的转换为相应byte类型,第二遍才是利用byte执行进行比较。
为什么第一遍时必须既比较 hashCode,又利用equals比较呢?hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突,例如 BMC. 这两个字符串的 hashCode 值都是 2123,如果有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void choose(String str){
byte x=-1;
switch(str.hashCode()){
case 99162322://hello的hashCode
if(str.equals("hello")){
x = 0;
}
break;
case 113318802://world的hashCode
if(str.equals("world")){
x = 1;
}
}
switch(x){
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
}
}

7、switch 枚举类

  • switch 枚举的例子,原始代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class JVMStack {
public static void foo(Sex sex) {
switch (sex) {
case MALE:
System.out.println("男");
break;
case FEMALE:
System.out.println("女");
break;
}
}
}
enum Sex {
MALE,FEMALE
}
  • 转换后的代码

会生成一个静态内部类(合成类),这个内部类仅被 JVM 使用,对我们不可见,同时在静态内部类中定义一个数组,用于存储枚举中的各种情况。

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
public class JVMStack {
/**
*定义一个合成类(仅jvm使用,对我们不可见)
*用来映射枚举的ordinal与数组元素的关系
*枚举的ordinal表示枚举对象的序号,从0开始
*即MALE的ordinal()=0,FEMALE的ordinal()=1
*/
static class $MAP{
//数组大小即为枚举元素个数,里面存储case用来对比的数字
static int[] map = new int[2];
static{
map[Sex.MALE.ordinal()]=1;
map[Sex.FEMALE.ordinal()]=2;
}
}
public static void foo(Sex sex){
int x = $MAP.map[sex.ordinal()];
switch(x){
case 1:
System.out.println("男");
break;
case 2:
System.out.println("女");
break;
}
}
}
enum Sex {
MALE,FEMALE
}

8、枚举类

JDK 7 新增了枚举类,以前面的性别枚举为例,枚举类实际上是一个 class,枚举类中的每个枚举都是一个类实例对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public final class Sex extends Enum<Sex>{
public static final Sex MALE;
public static final Sex FEMALE;
private static final Sex[]$VALUES;
static{
MALE=new Sex("MALE",0);
FEMALE=new Sex("FEMALE",1);
$VALUES=new Sex[]{MALE,FEMALE};
}
private Sex(String name,int ordinal){
super(name,ordinal);
}
public static Sex[]values(){
return $VALUES.clone();
}
public static Sex valueOf(String name){
return Enum.valueOf(Sex.class,name);
}
}

9、try-with-resources

在 JDK 7 开始新增了对需要关闭的资源处理的方法,即 try-with-resources

1
2
3
4
try(资源变量 = 创建资源对象) {
} catch () {

}

其中资源对象需要实现 AutoCloseable 接口,例如 InputStreamOutputStreamConnectionStatementResultSet 等接口都实现了 AutoCloseable ,使用try-with-resources可以不用写 finally 语句块,编译器会帮助生成关闭资源代码,例如:

1
2
3
4
5
try(InputStream is = new FileInputStream("test.txt")) {
System.out.println(is);
} catch (IOException e) {
e.printStackTrace();
}
  • 上面的代码会被转换为
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
try {
InputStream is = new FileInputStream("test.txt");
Throwable t = null;
try {
System.out.println(is);
} catch (Throwable e1) {
//t是我们代码出现的异常
t = e1;
throw e1;
} finally {
//判断了资源不为空
if (is != null) {
//如果我们代码有异常
if (t != null) {
try {
is.close();
} catch (Throwable e2) {
//如果close出现异常,作为被压制异常添加
t.addSuppressed(e2);
}
} else {
//如果我们代码没有异常,close出现的异常就是最后catch块中的e
is.close();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}

10、方法重写时的桥接方法

我们都知道,方法重写时对返回值分两种情况:

  1. 父子类的返回值完全一致
  2. 子类返回值可以是父类返回值的子类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class JVMStack {
public Number m() {
return 1;
}

}

class B extends JVMStack {
@Override
// 子类 m 方法的返回值 Integer 父类 m 方法返回值的子类
public Integer m() {
return 2;
}
}
  • 对于子类,编译器对做以下处理
1
2
3
4
5
6
class B extends JVMStack {
@Override
public synthetic bridge Number() {
return m();
}
}

其中桥接方法比较特殊,仅对java虚拟机可见,并且与原来的public Integer m()没有命名冲突

11、匿名内部类

  • 源代码
1
2
3
4
5
6
7
8
9
10
public class JVMStack {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok!");
}
};
}
}
  • 转换后代码

此时生成了一个额外的类

1
2
3
4
5
6
final class JVMStack$1 implements Runnable {
JVMStack$1() {}
public void run() {
System.out.println("ok!");
}
}
  • 同时在类中创建了一个类对象
1
2
3
4
5
public class JVMStack {
public static void main(String[] args) {
Runnable runnable = new JVMStack$1();
}
}

4.4、类加载阶段

1、加载

将类的字节码载入方法区中,内部采用C++的 instanceKlass 描述java类,它的重要field有:

  • _java_mirror 即java的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给java使用
  • _super 即父类
  • _fields 即成员变量
  • methods 即方法
  • _constants 即常量池
  • class_loader 即类加载器
  • _vtable 虚方法表
  • _itable 接口方法表

如果这个类还有父类没有被加载,那么先加载父类

加载和连接可能是交替运行的。

  • 注意:

instanceKlass 这样的【元数据】是存储在方法区(1.8后的元空间内),但_java_mirror是存储在堆中

image-20210527214815713

2、链接

验证类是否符合 JVM 规范,安全性检查

UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行

  • 准备

为 static 变量分配空间,设置默认值

  1. static变量在JDK 7之前存储于instanceKlass末尾,从JDK 7开始,存储于_java_mirror末尾
  2. static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成赋值在初始化阶段完成
  3. 如果static变量是final的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
  4. 如果static变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成
  • 解析

将常量池中的符号引用解析为直接引用

3、初始化

  • < cinit >()v 方法

初始化即调用()V,虚拟机会保证这个类的『构造方法』的线程安全

  • 发生时机

概括的说,类初始化是【懒惰的】

  1. main 方法所在的类,总会被首先初始化
  2. 首次访问这个类的静态变量或静态方法时
  3. 子类初始化,如果父类还没初始化,会引发
  4. 子类访问父类的静态变量,只会触发父类的初始化
  5. Class.forName
  6. new 会导致初始化
  • 不会导致类初始化的情况
  1. 访问类的static final静态常量(基本类型和字符串)不会触发初始化
  2. 类对象.class不会触发初始化
  3. 创建该类的数组不会触发初始化
  4. 类加载器的 loadClass 方法
  5. Class.forName的参数2为false时

4.5、类加载器

1、概述

JDK 8 为例,JVM 中的类加载器大致可以分为以下四种,其中Bootstrap ClassLoaderExtension ClassLoaderApplication ClassLoader 三个类加载器各司其职,各自加载属于自己管理范畴的类。

名称加载哪里的类说明
Bootstrap ClassLoaderJAVA_HOME/jre/librt.jar无法直接访问(CPP 实现),显示为 null
Extension ClassLoaderJAVA_HOME/jre/lib/ext上级为 Bootstrap ClassLoader
Application ClassLoaderclasspath(类路径)上级为 Extension ClassLoader
自定义类加载器自定义上级为 Application ClassLoader
  • 如果我们自定义一个 lang.String ,那么这个自定义的 String 类会被加载吗?

不会,在加载我们自定义类时,不会直接加载我们自己写的 String 类,而是先委托 Extension ClassLoader 去加载,然后 Extension ClassLoader 会委托他的上级,也就是 Bootstrap ClassLoader 去加载,此时 Bootstrap ClassLoader 会加载其管辖范围内的 rt.jar 下的 String 类,同时标记 String 类已被加载,此时就不会加载我们类路径下自定义的 String 类了。

这其中涉及了类加载的双亲委派机制。

在加载我们自定义的 Student 类时,Application ClassLoader 不会直接加载这个 Student 类,而是向上级一层一层委派(直到最顶层),当最顶层的类也无法加载这个 Student 类时,才会下放加载权给下级类加载器,直到最后由 Application ClassLoader 加载这个 String 类。

2、启动类加载器 (Bootstrap ClassLoader

我们可以通过设置一些虚拟机参数来将我们自定义的类交由启动类加载器加载

  • 编写一个 F 类,这个类将交予启动类加载器加载
1
2
3
4
5
public class F {
static {
System.out.println("bootstrap classLoader init class F");
}
}
  • 使用 Class.forName 加载这个类并判断这个类由哪个类加载器加载
1
2
3
4
5
6
public class TestJVM {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> clazz = Class.forName("com.hzx.decimal.F");
System.out.println(clazz.getClassLoader());
}
}
  • 在终端中执行以下命令
1
java -Xbootclasspath/a:. com.hzx.decimal.TestJVM
  • 结果
1
2
bootstrap classLoader init class F
null
  • 显示结果为 null

由于启动类加载器使用 CPP 实现,所以无法直接访问,直接显示为 null

3、双亲委派模式

所谓的是双亲委派,就是指调用类加载器的 loadClass 的方法时,查找类的规则

注意:这里的双亲,翻译为上级似乎更加合适,因为它们之间并没有继承关系。

  • 查看 ClassLoader 类中的 loadClass 方法源码
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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 第一步:检查这个类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 第二步:如果有上级,那么需要委托上级进行 loadClass
c = parent.loadClass(name, false);
} else {
// 第三步:如果没有上级了(ExtClassLoader),则委派 BootstrapClassLoader ,findBootstrapClassOrNull 方法让启动类加载器去 JAVA_HOME/jre/lib 下去加载类,如果类不在 JAVA_HOME/jre/lib 下,就返回null,该方法的底层实现方法使用 native 修饰
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}

if (c == null) {
// 第四步:如果每一层都找不到,那么调用 findClass 方法(每个类加载器自己扩展)来加载
long t1 = System.nanoTime();
c = findClass(name);

// 第五步:记录耗时
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
// 如果找到类,那么return,否则报 ClassNotFound 异常。
return c;
}
}

4、线程上下文类加载器

我们在使用 JDBC 时,都需要加载 Driver 驱动,实际上我们不写

1
Class.forName("com.mysql.jdbc.Driver");

也可以让 com.mysql.jdbc.Driver 正确加载

  • 我们可以追踪一下源码
1
2
3
4
5
6
7
8
9
public class DriverManager {
// 用于注册驱动的集合
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
// 初始化驱动
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
}
  • 查看 DriverManager 的类加载器
1
System.out.println(DriverManager.class.getClassLoader());
  • 结果

image-20210529103502760

打印 NULL ,表示它的类加载器是 Bootstrap ClassLoader ,会到 JAVA_HOME/jre/lib 下搜索类,但 JAVA_HOME/jre/lib 中并没有 mysql-connector-java-5.1.47.jar

那么 com.mysql.jdbc.Driver 是如何正确加载的?

  • 查看 loadInitialDrivers() 方法
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
  private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}

// 1 使用一个ServiceLoader机制加载驱动,即 SPI
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {

}
return null;
}
});

println("DriverManager.initialize: jdbc.drivers = " + drivers);
// 2 使用 jdbc.drivers 定义的驱动名加载驱动
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
// 3 这里的 ClassLoader.getSystemClassLoader() 就是英语程序类加载器,打破了双亲委派机制(没有委托上级,而是直接使用应用程序类加载器)
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}

在某些情况下,JDK 需要打破自己定义的双亲委派机制,否则会找不到需要加载的类。

  • 查看上面的 Service Provider Interface (SPI)

约定如下,在 jar 包下的 META-INF/services 包下,以接口全限定名为文件名,文件内容为实现类名称。

image-20210529105051688

这样就可以使用

1
2
3
4
5
ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while(iter.hasNext()) {
iter.next();
}

来得到实体类,体现了【面向接口编程 + 解耦】的思想,在很多框架中都运用了这个思想:

  1. JDBC
  2. Servlet 初始化器
  3. Spring 容器
  4. Dubbo (SPI 扩展)
  • 接着看 ServiceLoader.load 方法
1
2
3
4
5
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前线程的线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}

线程上下文类加载器是当前线程使用的类加载器,默认是应用程序类加载器,它内部又由 Class.forName() 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类 LazyIterator 中:

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
private class LazyIterator
implements Iterator<S>
{

Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;

private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}

private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}

private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}

public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}

public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}

public void remove() {
throw new UnsupportedOperationException();
}

}

5、自定义类加载器

  • 什么时候需要自定义类加载器?
  1. 想加载非 classpath 随意路径中的类文件

  2. 都是通过接口来实现,希望解耦时,常用于框架设计。

  3. 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

  • 步骤
  1. 继承 ClassLoader 父类
  2. 遵从双亲委派机制,重写 findClass 方法

注意不是重写 loadClass 方法,否则不会走双亲委派机制

  1. 读取类文件的字节码
  2. 调用父类的 defineClass 方法来加载类
  3. 使用者调用该类加载器的 loadClass 方法
  • 实例

创建一个类,这个类需要继承 ClassLoader,重写 findClass 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyClassLoader extends ClassLoader {
/**
*
* @param name 类名称
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//1 定义类字节码文件的全路径
String path = "e:\\myClasspath\\" + name + ".class";
try(ByteArrayOutputStream os = new ByteArrayOutputStream()) {
Files.copy(Paths.get(path),os);
// 得到字节数组
byte[] bytes = os.toByteArray();
// byte[] -> *.class
return defineClass(name,bytes,0,bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException("类文件未找到!");
}
}
}

认为类是否完全一致的两个因素:

  1. 类的全限定名相同
  2. 加载的类加载器一样

如果一个类由同一个类加载器的不同对象加载,那么也认为得到的 Class 对象不是同一个。

4.6、运行期优化

1、即时编译–分层编译

  • 运行以下代码
1
2
3
4
5
6
7
8
9
10
11
12
public class JVMStack {
public static void main(String[] args) {
for (int i = 0; i < 200; i++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
new Object();
}
long end = System.nanoTime();
System.out.printf("%d\t%d\n",i,(end - start));
}
}
}

可以看到,在120次循环后,所用时间明显下降

image-20210529134422496

  • 出现上述情况的原因是什么呢?

JVM 将执行状态分为了 5 个层次

  1. 0层,解释执行(Interpreter)
  2. 1层,使用C1即时编译器编译执行(不带profiling)
  3. 2层,使用C1即时编译器编译执行(带基本的profiling)
  4. 3层,使用C1即时编译器编译执行(带完全的profiling)
  5. 4层,使用C2即时编译器编译执行

profiling是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等

  • 即时编译器(JIT)和解释器的区别
  1. 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
  2. JIT是将一些字节码编译为机器码,并存入Code Cache,下次遇到相同的代码,直接执行,无需再编译
  3. 解释器是将字节码解释为针对所有平台都通用的机器码
  4. JIT会根据平台类型,生成平台特定的机器码

对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;

另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。执行效率上简单比较一下Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由来),优化之.

刚才的一种优化手段称之为【逃逸分析】,发现新建的对象是否逃逸。可以使用 -XX:-DoEscapeAnalysis 关闭逃逸分析,再运行刚才的示例观察结果

2、即时编译–方法内联

  • 编写一个求平方数的函数
1
2
3
4
5
6
7
8
9
public class JVMStack {
public static void main(String[] args) {
System.out.println(square(9));
}

private static int square(int i) {
return i * i;
}
}

如果发现 square() 方法是热带你方法,且代码长度不算长时,此时会进行方法内联,所谓的方法内联就是将方法内代码拷贝,粘贴到方法调用者的位置,所以上面的代码变为

1
2
3
public static void main(String[] args) {
System.out.println(9 * 9);
}