Skip to content

Commit 1993685

Browse files
committed
[docs update]完善类加载过程详解
1 parent 78fee25 commit 1993685

6 files changed

+70
-31
lines changed

docs/java/concurrent/java-concurrent-questions-02.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,7 @@ public ReentrantReadWriteLock(boolean fair) {
579579

580580
### 读锁为什么不能升级为写锁?
581581

582-
写锁可以降级为读锁,但是读锁却不能升级为写锁。这是因为写锁降级为读锁会引起线程的争夺,毕竟写锁属于是独占锁,这样的话,会影响性能。
582+
写锁可以降级为读锁,但是读锁却不能升级为写锁。这是因为读锁升级为写锁会引起线程的争夺,毕竟写锁属于是独占锁,这样的话,会影响性能。
583583

584584
另外,还可能会有死锁问题发生。举个例子:假设两个线程的读锁都想升级写锁,则需要对方都释放自己锁,而双方都不释放,就会产生死锁。
585585

@@ -657,7 +657,7 @@ public long tryOptimisticRead() {
657657
- [AQS 详解](https://javaguide.cn/java/concurrent/aqs.html)
658658
- [StampedLock 底层原理分析](https://segmentfault.com/a/1190000015808032)
659659

660-
如果你只是准备面试的话,建议多花点经历搞懂 AQS 原理即可,StampedLock 底层原理在面试中遇到的概率非常小。
660+
如果你只是准备面试的话,建议多花点精力搞懂 AQS 原理即可,`StampedLock` 底层原理在面试中遇到的概率非常小。
661661

662662
## Atomic 原子类
663663

docs/java/jvm/class-loading-process.md

+59-25
Original file line numberDiff line numberDiff line change
@@ -7,67 +7,101 @@ tag:
77

88
## 类的生命周期
99

10-
类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为7个阶段::加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。
10+
类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段::加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,前三个阶段可以统称为连接(Linking)。
11+
12+
这 7 个阶段的顺序如下图所示:
1113

1214
![一个类的完整生命周期](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/jvm/lifecycle-of-a-class.png)
1315

1416
## 类加载过程
1517

16-
Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?
18+
**Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?**
1719

1820
系统加载 Class 类型的文件主要三步:**加载->连接->初始化**。连接过程又可分为三步:**验证->准备->解析**
1921

2022
![类加载过程](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/jvm/class-loading-procedure.png)
2123

22-
详见:[jvm规范5.4](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.4)
23-
24-
![](https://img-blog.csdnimg.cn/20210607102244508.png)
24+
详见 [Java Virtual Machine Specification - 5.3. Creation and Loading](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.3 "Java Virtual Machine Specification - 5.3. Creation and Loading")
2525

2626
### 加载
2727

2828
类加载过程的第一步,主要完成下面 3 件事情:
2929

30-
1. 通过全类名获取定义此类的二进制字节流
31-
2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
32-
3. 在内存中生成一个代表该类的 `Class` 对象,作为方法区这些数据的访问入口
30+
1. 通过全类名获取定义此类的二进制字节流。
31+
2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
32+
3. 在内存中生成一个代表该类的 `Class` 对象,作为方法区这些数据的访问入口。
33+
34+
虚拟机规范上面这 3 点并不具体,因此是非常灵活的。比如:"通过全类名获取定义此类的二进制字节流" 并没有指明具体从哪里获取( `ZIP``JAR``EAR``WAR` 、网络、动态代理技术运行时动态生成、其他文件生成比如 `JSP`...)、怎样获取。
3335

34-
虚拟机规范上面这 3 点并不具体,因此是非常灵活的。比如:"通过全类名获取定义此类的二进制字节流" 并没有指明具体从哪里获取、怎样获取。比如:比较常见的就是从 `ZIP` 包中读取(日后出现的 `JAR``EAR``WAR` 格式的基础)、其他文件生成(典型应用就是 `JSP`)等等
36+
加载这一步主要是通过我们后面要讲到的 **类加载器** 完成的。类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载由 **双亲委派模型** 决定(不过,我们也能打破由双亲委派模型)
3537

36-
一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 `loadClass()` 方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建
38+
> 类加载器、双亲委派模型也是非常重要的知识点,这部分内容在[类加载器详解](https://javaguide.cn/java/jvm/classloader.html "类加载器详解")这篇文章中有详细介绍到。阅读本篇文章的时候,大家知道有这么个东西就可以了
3739
38-
> 类加载器、双亲委派模型也是非常重要的知识点,这部分内容会在后面的文章中单独介绍到
40+
每个 Java 类都有一个引用指向加载它的 `ClassLoader`。不过,数组类不是通过 `ClassLoader` 创建的,而是 JVM 在需要的时候自动创建的,数组类通过`getClassLoader()`方法获取 `ClassLoader` 的时候和该数组的元素类型的 `ClassLoader` 是一致的
3941

40-
加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。
42+
一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 `loadClass()` 方法)。
43+
44+
加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。
4145

4246
### 验证
4347

44-
![验证阶段示意图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/验证阶段.png)
48+
**验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。**
49+
50+
验证阶段这一步在整个类加载过程中耗费的资源还是相对较多的,但很有必要,可以有效防止恶意代码的执行。任何时候,程序安全都是第一位。
51+
52+
不过,验证阶段也不是必须要执行的阶段。如果程序运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用 `-Xverify:none` 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
53+
54+
验证阶段主要由四个检验阶段组成:
55+
56+
1. 文件格式验证(Class 文件格式检查)
57+
2. 元数据验证(字节码语义检查)
58+
3. 字节码验证(程序语义检查)
59+
4. 符号引用验证(类的正确性检查)
60+
61+
![验证阶段示意图](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/jvm/class-loading-process-verification.png)
62+
63+
文件格式验证这一阶段是基于该类的二进制字节流进行的,主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java 类型信息的要求。除了这一阶段之外,其余三个验证阶段都是基于方法区的存储结构上进行的,不会再直接读取、操作字节流了。
64+
65+
> 方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 **类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据**
66+
>
67+
> 关于方法区的详细介绍,推荐阅读 [Java 内存区域详解](https://javaguide.cn/java/jvm/memory-area.html "Java 内存区域详解") 这篇文章。
68+
69+
符号引用验证发生在类加载过程中的解析阶段,具体点说是 JVM 将符号引用转化为直接引用的时候(解析阶段会介绍符号引用和直接引用)。
70+
71+
符号引用验证的主要目的是确保解析阶段能正常执行,如果无法通过符号引用验证,JVM 会抛出异常,比如:
72+
73+
- `java.lang.IllegalAccessError`:当类试图访问或修改它没有权限访问的字段,或调用它没有权限访问的方法时,抛出该异常。
74+
- `java.lang.NoSuchFieldError`:当类试图访问或修改一个指定的对象字段,而该对象不再包含该字段时,抛出该异常。
75+
- `java.lang.NoSuchMethodError`:当类试图访问一个指定的方法,而该方法不存在时,抛出该异常。
76+
- ......
4577

4678
### 准备
4779

4880
**准备阶段是正式为类变量分配内存并设置类变量初始值的阶段**,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
4981

5082
1. 这时候进行内存分配的仅包括类变量( Class Variables ,即静态变量,被 `static` 关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
51-
2. 从概念上讲,类变量所使用的内存都应当在 **方法区** 中进行分配。不过有一点需要注意的是:JDK 7 之前,HotSpot 使用永久代来实现方法区的时候,实现是完全符合这种逻辑概念的。 而在 JDK 7 及之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。相关阅读:[深入理解Java虚拟机(第3版)》勘误#75](https://github.com/fenixsoft/jvm_book/issues/75)
83+
2. 从概念上讲,类变量所使用的内存都应当在 **方法区** 中进行分配。不过有一点需要注意的是:JDK 7 之前,HotSpot 使用永久代来实现方法区的时候,实现是完全符合这种逻辑概念的。 而在 JDK 7 及之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。相关阅读:[深入理解 Java 虚拟机(第 3 版)》勘误#75](https://github.com/fenixsoft/jvm_book/issues/75 "《深入理解Java虚拟机(第3版)》勘误#75")
5284
3. 这里所设置的初始值"通常情况"下是数据类型默认的零值(如 0、0L、null、false 等),比如我们定义了`public static int value=111` ,那么 value 变量在准备阶段的初始值就是 0 而不是 111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 final 关键字`public static final int value=111` ,那么准备阶段 value 的值就被赋值为 111。
5385

54-
**基本数据类型的零值** : (图片来自《深入理解 Java 虚拟机》第 3 版 7.33 )
55-
56-
86+
**基本数据类型的零值** : (图片来自《深入理解 Java 虚拟机》第 3 版 7.33 )
5787

5888
![基本数据类型的零值](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/基本数据类型的零值.png)
5989

6090
### 解析
6191

62-
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。
92+
**解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。** 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。
93+
94+
《深入理解 Java 虚拟机》7.34 节第三版对符号引用和直接引用的解释如下:
95+
96+
![符号引用和直接引用](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/jvm/symbol-reference-and-direct-reference.png)
6397

64-
符号引用就是一组符号来描述目标,可以是任何字面量。**直接引用**就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。在程序实际运行时,只有符号引用是不够的,举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。
98+
举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。
6599

66100
综上,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。
67101

68102
### 初始化
69103

70-
初始化阶段是执行初始化方法 `<clinit> ()`方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。
104+
**初始化阶段是执行初始化方法 `<clinit> ()`方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。**
71105

72106
> 说明: `<clinit> ()`方法是编译之后自动生成的。
73107
@@ -85,13 +119,13 @@ Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚
85119
4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 `main` 方法的那个类),虚拟机会先初始化这个类。
86120
5. `MethodHandle``VarHandle` 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,
87121
就必须先使用 `findStaticVarHandle` 来初始化要调用的类。
88-
6. **「补充,来自[issue745](https://github.com/Snailclimb/JavaGuide/issues/745)** 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
122+
6. **「补充,来自[issue745](https://github.com/Snailclimb/JavaGuide/issues/745 "issue745")** 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
89123

90124
## 类卸载
91125

92-
> 卸载这部分内容来自 [issue#662](https://github.com/Snailclimb/JavaGuide/issues/662)**[guang19](https://github.com/guang19)** 补充完善。
126+
> 卸载这部分内容来自 [issue#662](https://github.com/Snailclimb/JavaGuide/issues/662 "issue#662")**[guang19](https://github.com/guang19 "guang19")** 补充完善。
93127
94-
卸载类即该类的 Class 对象被 GC。
128+
**卸载类即该类的 Class 对象被 GC。**
95129

96130
卸载类需要满足 3 个要求:
97131

@@ -101,10 +135,10 @@ Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚
101135

102136
所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。
103137

104-
只要想通一点就好了,jdk 自带的 `BootstrapClassLoader`, `ExtClassLoader`, `AppClassLoader` 负责加载 jdk 提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。
138+
只要想通一点就好了,JDK 自带的 `BootstrapClassLoader`, `ExtClassLoader`, `AppClassLoader` 负责加载 JDK 提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。
105139

106140
**参考**
107141

108142
- 《深入理解 Java 虚拟机》
109143
- 《实战 Java 虚拟机》
110-
- <https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html>
144+
- Chapter 5. Loading, Linking, and Initializing - Java Virtual Machine Specification:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.4

docs/java/jvm/classloader.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ tag:
3838

3939
> 类加载器是一个负责加载类的对象。`ClassLoader` 是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。
4040
>
41-
> 每个 Java 类都有一个引用指向加载它的 `ClassLoader`。不过,数组类不是通过 `ClassLoader` 创建的,而是通过 JVM 在需要的时候自动创建的,数组类通过`getClassLoader()`方法获取 `ClassLoader` 的时候和该数组的元素类型的 `ClassLoader` 是一致的。
41+
> 每个 Java 类都有一个引用指向加载它的 `ClassLoader`。不过,数组类不是通过 `ClassLoader` 创建的,而是 JVM 在需要的时候自动创建的,数组类通过`getClassLoader()`方法获取 `ClassLoader` 的时候和该数组的元素类型的 `ClassLoader` 是一致的。
4242
4343
从上面的介绍可以看出:
4444

0 commit comments

Comments
 (0)