Java 虚拟机.md 39.6 KB
Newer Older
C
CyC2018 已提交
1
* [点击阅读面试进阶指南 ](https://github.com/CyC2018/Backend-Interview-Guide)
C
CyC2018 已提交
2 3
<!-- GFM-TOC -->
* [一、运行时数据区域](#一运行时数据区域)
C
CyC2018 已提交
4
    * [程序计数器](#程序计数器)
C
CyC2018 已提交
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
    * [程序计数器](#程序计数器)
    * [Java 虚拟机栈](#java-虚拟机栈)
    * [本地方法栈](#本地方法栈)
    * [](#堆)
    * [方法区](#方法区)
    * [运行时常量池](#运行时常量池)
    * [直接内存](#直接内存)
* [二、垃圾收集](#二垃圾收集)
    * [判断一个对象是否可被回收](#判断一个对象是否可被回收)
    * [引用类型](#引用类型)
    * [垃圾收集算法](#垃圾收集算法)
    * [垃圾收集器](#垃圾收集器)
* [三、内存分配与回收策略](#三内存分配与回收策略)
    * [Minor GC 和 Full GC](#minor-gc-和-full-gc)
    * [内存分配策略](#内存分配策略)
    * [Full GC 的触发条件](#full-gc-的触发条件)
* [四、类加载机制](#四类加载机制)
    * [类的生命周期](#类的生命周期)
    * [类加载过程](#类加载过程)
    * [类初始化时机](#类初始化时机)
    * [类与类加载器](#类与类加载器)
    * [类加载器分类](#类加载器分类)
    * [双亲委派模型](#双亲委派模型)
    * [自定义类加载器实现](#自定义类加载器实现)
C
CyC2018 已提交
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
* [参考资料](#参考资料)
    * [本地方法栈](#本地方法栈)
    * [](#堆)
    * [方法区](#方法区)
    * [运行时常量池](#运行时常量池)
    * [直接内存](#直接内存)
* [二、垃圾收集](#二垃圾收集)
    * [判断一个对象是否可被回收](#判断一个对象是否可被回收)
    * [引用类型](#引用类型)
    * [垃圾收集算法](#垃圾收集算法)
    * [垃圾收集器](#垃圾收集器)
* [三、内存分配与回收策略](#三内存分配与回收策略)
    * [Minor GC 和 Full GC](#minor-gc-和-full-gc)
    * [内存分配策略](#内存分配策略)
    * [Full GC 的触发条件](#full-gc-的触发条件)
* [四、类加载机制](#四类加载机制)
    * [类的生命周期](#类的生命周期)
    * [类加载过程](#类加载过程)
    * [类初始化时机](#类初始化时机)
    * [类与类加载器](#类与类加载器)
    * [类加载器分类](#类加载器分类)
    * [双亲委派模型](#双亲委派模型)
    * [自定义类加载器实现](#自定义类加载器实现)
C
CyC2018 已提交
52 53 54 55 56 57
* [参考资料](#参考资料)
<!-- GFM-TOC -->


# 一、运行时数据区域

C
CyC2018 已提交
58 59 60 61 62 63 64 65 66
<div align="center"> <img src="pics/14ab3de0-0d48-4466-9ea7-90b9be822034_200.png" width="500px"> </div><br>

## 程序计数器

记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。

## Java 虚拟机栈# 一、运行时数据区域

<div align="center"> <img src="pics/14ab3de0-0d48-4466-9ea7-90b9be822034_200.png" width="500px"> </div><br>
C
CyC2018 已提交
67 68

## 程序计数器
C
CyC2018 已提交
69 70 71

记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。

C
CyC2018 已提交
72
## Java 虚拟机栈
C
CyC2018 已提交
73

C
CyC2018 已提交
74
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
C
CyC2018 已提交
75

C
CyC2018 已提交
76
<div align="center"> <img src="pics/ff5b89ac-798e-4fbc-b0ce-da2fc2358570.jpg"/> </div><br>
C
CyC2018 已提交
77

C
CyC2018 已提交
78
可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小:
C
CyC2018 已提交
79 80

```java
C
CyC2018 已提交
81
java -Xss512M HackTheJava
C
CyC2018 已提交
82 83 84 85
```

该区域可能抛出以下异常:

C
CyC2018 已提交
86 87
- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
- 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
C
CyC2018 已提交
88

C
CyC2018 已提交
89
## 本地方法栈
C
CyC2018 已提交
90

C
CyC2018 已提交
91
本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
C
CyC2018 已提交
92

C
CyC2018 已提交
93
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
C
CyC2018 已提交
94

C
CyC2018 已提交
95
<div align="center"> <img src="pics/1_2001550547261811.png"/> </div><br>
C
CyC2018 已提交
96

C
CyC2018 已提交
97
## 堆
C
CyC2018 已提交
98

C
CyC2018 已提交
99
所有对象都在这里分配内存,是垃圾收集的主要区域("GC 堆")。
C
CyC2018 已提交
100

C
CyC2018 已提交
101
现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:
C
CyC2018 已提交
102

C
CyC2018 已提交
103 104
- 新生代(Young Generation)
- 老年代(Old Generation)
C
CyC2018 已提交
105

C
CyC2018 已提交
106
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
C
CyC2018 已提交
107

C
CyC2018 已提交
108
可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
C
CyC2018 已提交
109 110

```java
C
CyC2018 已提交
111
java -Xms1M -Xmx2M HackTheJava
C
CyC2018 已提交
112 113
```

C
CyC2018 已提交
114
## 方法区
C
CyC2018 已提交
115 116 117

用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

C
CyC2018 已提交
118
和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
C
CyC2018 已提交
119

C
CyC2018 已提交
120 121
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。

C
CyC2018 已提交
122
HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
C
CyC2018 已提交
123

C
CyC2018 已提交
124
## 运行时常量池
C
CyC2018 已提交
125 126 127

运行时常量池是方法区的一部分。

C
CyC2018 已提交
128
Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。
C
CyC2018 已提交
129

C
CyC2018 已提交
130
除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。
C
CyC2018 已提交
131

C
CyC2018 已提交
132
## 直接内存
C
CyC2018 已提交
133

C
CyC2018 已提交
134
在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。
C
CyC2018 已提交
135

C
CyC2018 已提交
136
# 二、垃圾收集
C
CyC2018 已提交
137

C
CyC2018 已提交
138
垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。
C
CyC2018 已提交
139

C
CyC2018 已提交
140
## 判断一个对象是否可被回收
C
CyC2018 已提交
141

C
CyC2018 已提交
142
### 1. 引用计数算法
C
CyC2018 已提交
143

C
CyC2018 已提交
144
为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
C
CyC2018 已提交
145

C
CyC2018 已提交
146
在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
C
CyC2018 已提交
147

C
CyC2018 已提交
148
```java
C
CyC2018 已提交
149
public class Test {
C
CyC2018 已提交
150

C
CyC2018 已提交
151
    public Object instance = null;
C
CyC2018 已提交
152

C
CyC2018 已提交
153
    public static void main(String[] args) {
C
CyC2018 已提交
154 155 156 157
        Test a = new Test();
        Test b = new Test();
        a.instance = b;
        b.instance = a;
C
CyC2018 已提交
158
    }
C
CyC2018 已提交
159
}
C
CyC2018 已提交
160 161
```

C
CyC2018 已提交
162
### 2. 可达性分析算法
C
CyC2018 已提交
163

C
CyC2018 已提交
164
以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
C
CyC2018 已提交
165

C
CyC2018 已提交
166
Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下内容:
C
CyC2018 已提交
167

C
CyC2018 已提交
168 169 170 171
- 虚拟机栈中局部变量表中引用的对象
- 本地方法栈中 JNI 中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
C
CyC2018 已提交
172

C
CyC2018 已提交
173
<div align="center"> <img src="pics/6dd28bfc-6ef7-47cb-af50-a681ebc1bbaa.png"/> </div><br>
C
CyC2018 已提交
174

C
CyC2018 已提交
175
### 3. 方法区的回收
C
CyC2018 已提交
176

C
CyC2018 已提交
177
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。
C
CyC2018 已提交
178 179 180

主要是对常量池的回收和对类的卸载。

C
CyC2018 已提交
181
为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。
C
CyC2018 已提交
182

C
CyC2018 已提交
183
类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:
C
CyC2018 已提交
184

C
CyC2018 已提交
185 186 187
- 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
C
CyC2018 已提交
188

C
CyC2018 已提交
189
### 4. finalize()
C
CyC2018 已提交
190

C
CyC2018 已提交
191
类似 C++ 的析构函数,用于关闭外部资源。但是 try-finally 等方式可以做得更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
C
CyC2018 已提交
192

C
CyC2018 已提交
193
当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。
C
CyC2018 已提交
194

C
CyC2018 已提交
195
## 引用类型
C
CyC2018 已提交
196

C
CyC2018 已提交
197
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
C
CyC2018 已提交
198

C
CyC2018 已提交
199
Java 提供了四种强度不同的引用类型。
C
CyC2018 已提交
200

C
CyC2018 已提交
201
### 1. 强引用
C
CyC2018 已提交
202

C
CyC2018 已提交
203
被强引用关联的对象不会被回收。
C
CyC2018 已提交
204

C
CyC2018 已提交
205
使用 new 一个新对象的方式来创建强引用。
C
CyC2018 已提交
206 207

```java
C
CyC2018 已提交
208
Object obj = new Object();
C
CyC2018 已提交
209 210
```

C
CyC2018 已提交
211
### 2. 软引用
C
CyC2018 已提交
212

C
CyC2018 已提交
213
被软引用关联的对象只有在内存不够的情况下才会被回收。
C
CyC2018 已提交
214

C
CyC2018 已提交
215
使用 SoftReference 类来创建软引用。
C
CyC2018 已提交
216 217

```java
C
CyC2018 已提交
218 219 220
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使对象只被软引用关联
C
CyC2018 已提交
221 222
```

C
CyC2018 已提交
223
### 3. 弱引用
C
CyC2018 已提交
224

C
CyC2018 已提交
225
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
C
CyC2018 已提交
226

C
CyC2018 已提交
227
使用 WeakReference 类来创建弱引用。
C
CyC2018 已提交
228 229

```java
C
CyC2018 已提交
230 231 232
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
C
CyC2018 已提交
233 234
```

C
CyC2018 已提交
235
### 4. 虚引用
C
CyC2018 已提交
236

C
CyC2018 已提交
237
又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。
C
CyC2018 已提交
238

C
CyC2018 已提交
239
为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。
C
CyC2018 已提交
240

C
CyC2018 已提交
241
使用 PhantomReference 来创建虚引用。
C
CyC2018 已提交
242 243

```java
C
CyC2018 已提交
244 245 246
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;
C
CyC2018 已提交
247 248
```

C
CyC2018 已提交
249
## 垃圾收集算法
C
CyC2018 已提交
250

C
CyC2018 已提交
251
### 1. 标记 - 清除
C
CyC2018 已提交
252

253
<div align="center"> <img src="pics/3_2001550547558008.png"/> </div><br>
C
CyC2018 已提交
254

C
CyC2018 已提交
255

C
CyC2018 已提交
256
标记要回收的对象,然后清除。
C
CyC2018 已提交
257 258 259

不足:

C
CyC2018 已提交
260 261
- 标记和清除过程效率都不高;
- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。
C
CyC2018 已提交
262

C
CyC2018 已提交
263
### 2. 标记 - 整理
C
CyC2018 已提交
264

C
CyC2018 已提交
265

266
<div align="center"> <img src="pics/2_2001550547456403.png"/> </div><br>
C
CyC2018 已提交
267

C
CyC2018 已提交
268

C
CyC2018 已提交
269 270
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

C
CyC2018 已提交
271
### 3. 复制
C
CyC2018 已提交
272

C
CyC2018 已提交
273
<div align="center"> <img src="pics/4_2001550547640585.png"/> </div><br>
C
CyC2018 已提交
274 275 276 277 278

将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。

主要不足是只使用了内存的一半。

C
CyC2018 已提交
279
现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。
C
CyC2018 已提交
280

C
CyC2018 已提交
281
HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。
C
CyC2018 已提交
282

C
CyC2018 已提交
283
### 4. 分代收集
C
CyC2018 已提交
284 285 286

现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。

C
CyC2018 已提交
287
一般将堆分为新生代和老年代。
C
CyC2018 已提交
288

C
CyC2018 已提交
289 290
- 新生代使用:复制算法
- 老年代使用:标记 - 清除 或者 标记 - 整理 算法
C
CyC2018 已提交
291

C
CyC2018 已提交
292
## 垃圾收集器
C
CyC2018 已提交
293

C
CyC2018 已提交
294
<div align="center"> <img src="pics/c625baa0-dde6-449e-93df-c3a67f2f430f.jpg" width=""/> </div><br>
C
CyC2018 已提交
295

C
CyC2018 已提交
296
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
C
CyC2018 已提交
297

C
CyC2018 已提交
298
- 单线程与多线程:单线程指的是垃圾收集器只使用一个线程,而多线程使用多个线程;
C
CyC2018 已提交
299
- 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
C
CyC2018 已提交
300

C
CyC2018 已提交
301
### 1. Serial 收集器
C
CyC2018 已提交
302

C
CyC2018 已提交
303
<div align="center"> <img src="pics/22fda4ae-4dd5-489d-ab10-9ebfdad22ae0.jpg" width=""/> </div><br>
C
CyC2018 已提交
304

C
CyC2018 已提交
305
Serial 翻译为串行,也就是说它以串行的方式执行。
C
CyC2018 已提交
306 307

它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
C
CyC2018 已提交
308

C
CyC2018 已提交
309
它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
C
CyC2018 已提交
310

C
CyC2018 已提交
311
它是 Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。
C
CyC2018 已提交
312

C
CyC2018 已提交
313
### 2. ParNew 收集器
C
CyC2018 已提交
314

C
CyC2018 已提交
315
<div align="center"> <img src="pics/81538cd5-1bcf-4e31-86e5-e198df1e013b.jpg" width=""/> </div><br>
C
CyC2018 已提交
316

C
CyC2018 已提交
317
它是 Serial 收集器的多线程版本。
C
CyC2018 已提交
318

C
CyC2018 已提交
319
它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。
C
CyC2018 已提交
320

C
CyC2018 已提交
321
### 3. Parallel Scavenge 收集器
C
CyC2018 已提交
322

C
CyC2018 已提交
323
与 ParNew 一样是多线程收集器。
C
CyC2018 已提交
324

C
CyC2018 已提交
325
其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。
C
CyC2018 已提交
326

C
CyC2018 已提交
327
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
C
CyC2018 已提交
328

C
CyC2018 已提交
329
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
C
CyC2018 已提交
330

C
CyC2018 已提交
331
可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
C
CyC2018 已提交
332

C
CyC2018 已提交
333
### 4. Serial Old 收集器
C
CyC2018 已提交
334

C
CyC2018 已提交
335
<div align="center"> <img src="pics/08f32fd3-f736-4a67-81ca-295b2a7972f2.jpg" width=""/> </div><br>
C
CyC2018 已提交
336

C
CyC2018 已提交
337
是 Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下,它有两大用途:
C
CyC2018 已提交
338

C
CyC2018 已提交
339 340
- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
C
CyC2018 已提交
341

C
CyC2018 已提交
342
### 5. Parallel Old 收集器
C
CyC2018 已提交
343

C
CyC2018 已提交
344
<div align="center"> <img src="pics/278fe431-af88-4a95-a895-9c3b80117de3.jpg" width=""/> </div><br>
C
CyC2018 已提交
345

C
CyC2018 已提交
346
是 Parallel Scavenge 收集器的老年代版本。
C
CyC2018 已提交
347

C
CyC2018 已提交
348
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
C
CyC2018 已提交
349

C
CyC2018 已提交
350
### 6. CMS 收集器
C
CyC2018 已提交
351

C
CyC2018 已提交
352
<div align="center"> <img src="pics/62e77997-6957-4b68-8d12-bfd609bb2c68.jpg" width=""/> </div><br>
C
CyC2018 已提交
353

C
CyC2018 已提交
354
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
C
CyC2018 已提交
355 356 357

分为以下四个流程:

C
CyC2018 已提交
358 359 360 361
- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
- 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
- 并发清除:不需要停顿。
C
CyC2018 已提交
362 363 364 365 366

在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。

具有以下缺点:

C
CyC2018 已提交
367 368 369
- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
C
CyC2018 已提交
370

C
CyC2018 已提交
371
### 7. G1 收集器
C
CyC2018 已提交
372

C
CyC2018 已提交
373
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
C
CyC2018 已提交
374

C
CyC2018 已提交
375
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
C
CyC2018 已提交
376

C
CyC2018 已提交
377
<div align="center"> <img src="pics/4cf711a8-7ab2-4152-b85c-d5c226733807.png" width="600"/> </div><br>
C
CyC2018 已提交
378

C
CyC2018 已提交
379
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
C
CyC2018 已提交
380

C
CyC2018 已提交
381
<div align="center"> <img src="pics/9bbddeeb-e939-41f0-8e8e-2b1a0aa7e0a7.png" width="600"/> </div><br>
C
CyC2018 已提交
382

C
CyC2018 已提交
383
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
C
CyC2018 已提交
384

C
CyC2018 已提交
385
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
C
CyC2018 已提交
386

C
CyC2018 已提交
387
<div align="center"> <img src="pics/f99ee771-c56f-47fb-9148-c0036695b5fe.jpg" width=""/> </div><br>
C
CyC2018 已提交
388

C
CyC2018 已提交
389
如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
C
CyC2018 已提交
390

C
CyC2018 已提交
391 392 393 394
- 初始标记
- 并发标记
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
C
CyC2018 已提交
395 396 397

具备如下特点:

C
CyC2018 已提交
398 399
- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
C
CyC2018 已提交
400

C
CyC2018 已提交
401
# 三、内存分配与回收策略
C
CyC2018 已提交
402

C
CyC2018 已提交
403
## Minor GC 和 Full GC
C
CyC2018 已提交
404

C
CyC2018 已提交
405
- Minor GC:回收新生代上,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
C
CyC2018 已提交
406

C
CyC2018 已提交
407
- Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
C
CyC2018 已提交
408

C
CyC2018 已提交
409
## 内存分配策略
C
CyC2018 已提交
410

C
CyC2018 已提交
411
### 1. 对象优先在 Eden 分配
C
CyC2018 已提交
412

C
CyC2018 已提交
413
大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
C
CyC2018 已提交
414

C
CyC2018 已提交
415
### 2. 大对象直接进入老年代
C
CyC2018 已提交
416

C
CyC2018 已提交
417
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
C
CyC2018 已提交
418

C
CyC2018 已提交
419
经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
C
CyC2018 已提交
420

C
CyC2018 已提交
421
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。
C
CyC2018 已提交
422

C
CyC2018 已提交
423
### 3. 长期存活的对象进入老年代
C
CyC2018 已提交
424

C
CyC2018 已提交
425
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
C
CyC2018 已提交
426

C
CyC2018 已提交
427
-XX:MaxTenuringThreshold 用来定义年龄的阈值。
C
CyC2018 已提交
428

C
CyC2018 已提交
429
### 4. 动态对象年龄判定
C
CyC2018 已提交
430

C
CyC2018 已提交
431
虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
C
CyC2018 已提交
432

C
CyC2018 已提交
433
### 5. 空间分配担保
C
CyC2018 已提交
434

C
CyC2018 已提交
435
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
C
CyC2018 已提交
436

C
CyC2018 已提交
437
如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。
C
CyC2018 已提交
438

C
CyC2018 已提交
439
## Full GC 的触发条件
C
CyC2018 已提交
440

C
CyC2018 已提交
441
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
C
CyC2018 已提交
442

C
CyC2018 已提交
443
### 1. 调用 System.gc()
C
CyC2018 已提交
444

C
CyC2018 已提交
445
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
C
CyC2018 已提交
446

C
CyC2018 已提交
447
### 2. 老年代空间不足
C
CyC2018 已提交
448

C
CyC2018 已提交
449
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
C
CyC2018 已提交
450

C
CyC2018 已提交
451
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
C
CyC2018 已提交
452

C
CyC2018 已提交
453
### 3. 空间分配担保失败
C
CyC2018 已提交
454

C
CyC2018 已提交
455
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第五小节。
C
CyC2018 已提交
456

C
CyC2018 已提交
457
### 4. JDK 1.7 及以前的永久代空间不足
C
CyC2018 已提交
458

C
CyC2018 已提交
459
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
C
CyC2018 已提交
460

C
CyC2018 已提交
461
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
C
CyC2018 已提交
462

C
CyC2018 已提交
463
为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
C
CyC2018 已提交
464

C
CyC2018 已提交
465
### 5. Concurrent Mode Failure
C
CyC2018 已提交
466

C
CyC2018 已提交
467
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
C
CyC2018 已提交
468

C
CyC2018 已提交
469
# 四、类加载机制
C
CyC2018 已提交
470

C
CyC2018 已提交
471
类是在运行期间第一次使用时动态加载的,而不是一次性加载。因为如果一次性加载,那么会占用很多的内存。
C
CyC2018 已提交
472

C
CyC2018 已提交
473
## 类的生命周期
C
CyC2018 已提交
474

C
CyC2018 已提交
475
<div align="center"> <img src="pics/303873db-0d11-4683-a43c-f319b7aef2b6.jpg"/> </div><br>
C
CyC2018 已提交
476

C
CyC2018 已提交
477
包括以下 7 个阶段:
C
CyC2018 已提交
478

C
CyC2018 已提交
479 480 481 482 483 484 485
-  **加载(Loading)** 
-  **验证(Verification)** 
-  **准备(Preparation)** 
-  **解析(Resolution)** 
-  **初始化(Initialization)** 
- 使用(Using)
- 卸载(Unloading)
C
CyC2018 已提交
486

C
CyC2018 已提交
487
## 类加载过程
C
CyC2018 已提交
488

C
CyC2018 已提交
489
包含了加载、验证、准备、解析和初始化这 5 个阶段。
C
CyC2018 已提交
490

C
CyC2018 已提交
491
### 1. 加载
C
CyC2018 已提交
492 493 494 495 496

加载是类加载的一个阶段,注意不要混淆。

加载过程完成以下三件事:

C
CyC2018 已提交
497 498 499 500
- 通过类的完全限定名称获取定义该类的二进制字节流。
- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
- 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口。

C
CyC2018 已提交
501 502 503

其中二进制字节流可以从以下方式中获取:

C
CyC2018 已提交
504 505 506 507
- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
- 从网络中获取,最典型的应用是 Applet。
- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。
C
CyC2018 已提交
508

C
CyC2018 已提交
509
### 2. 验证
C
CyC2018 已提交
510

C
CyC2018 已提交
511
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
C
CyC2018 已提交
512

C
CyC2018 已提交
513
### 3. 准备
C
CyC2018 已提交
514

C
CyC2018 已提交
515
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
C
CyC2018 已提交
516

C
CyC2018 已提交
517
实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
C
CyC2018 已提交
518

C
CyC2018 已提交
519
初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
C
CyC2018 已提交
520 521

```java
C
CyC2018 已提交
522
public static int value = 123;
C
CyC2018 已提交
523 524
```

C
CyC2018 已提交
525
如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0。例如下面的常量 value 被初始化为 123 而不是 0。
C
CyC2018 已提交
526 527

```java
C
CyC2018 已提交
528
public static final int value = 123;
C
CyC2018 已提交
529 530
```

C
CyC2018 已提交
531
### 4. 解析
C
CyC2018 已提交
532 533 534

将常量池的符号引用替换为直接引用的过程。

C
CyC2018 已提交
535
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
C
CyC2018 已提交
536

C
CyC2018 已提交
537 538
<div data="补充为什么可以支持动态绑定 --> <--"></div>

C
CyC2018 已提交
539
### 5. 初始化
C
CyC2018 已提交
540

C
CyC2018 已提交
541 542 543 544 545
<div data="modify -->"></div>

初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 &lt;clinit>() 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。

在准备阶段,已经为类变量分配了系统所需的初始值,并且在初始化阶段,根据程序员通过程序进行的主观计划来初始化类变量和其他资源。
C
CyC2018 已提交
546

C
CyC2018 已提交
547
&lt;clinit>() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
C
CyC2018 已提交
548 549

```java
C
CyC2018 已提交
550 551 552 553 554 555
public class Test {
    static {
        i = 0;                // 给变量赋值可以正常编译通过
        System.out.print(i);  // 这句编译器会提示“非法向前引用”
    }
    static int i = 1;
C
CyC2018 已提交
556 557 558
}
```

C
CyC2018 已提交
559
由于父类的 &lt;clinit>() 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码:
C
CyC2018 已提交
560 561

```java
C
CyC2018 已提交
562 563 564 565 566
static class Parent {
    public static int A = 1;
    static {
        A = 2;
    }
C
CyC2018 已提交
567 568
}

C
CyC2018 已提交
569 570
static class Sub extends Parent {
    public static int B = A;
C
CyC2018 已提交
571 572
}

C
CyC2018 已提交
573 574
public static void main(String[] args) {
     System.out.println(Sub.B);  // 2
C
CyC2018 已提交
575 576 577
}
```

C
CyC2018 已提交
578
接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 &lt;clinit>() 方法。但接口与类不同的是,执行接口的 &lt;clinit>() 方法不需要先执行父接口的 &lt;clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 &lt;clinit>() 方法。
C
CyC2018 已提交
579

C
CyC2018 已提交
580
虚拟机会保证一个类的 &lt;clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 &lt;clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 &lt;clinit>() 方法完毕。如果在一个类的 &lt;clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。
C
CyC2018 已提交
581

C
CyC2018 已提交
582
## 类初始化时机
C
CyC2018 已提交
583

C
CyC2018 已提交
584
### 1. 主动引用
C
CyC2018 已提交
585 586 587

虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):

C
CyC2018 已提交
588
- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
C
CyC2018 已提交
589

C
CyC2018 已提交
590
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
C
CyC2018 已提交
591

C
CyC2018 已提交
592
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
C
CyC2018 已提交
593

C
CyC2018 已提交
594
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
C
CyC2018 已提交
595

C
CyC2018 已提交
596
- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
C
CyC2018 已提交
597

C
CyC2018 已提交
598
### 2. 被动引用
C
CyC2018 已提交
599

C
CyC2018 已提交
600
以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
C
CyC2018 已提交
601

C
CyC2018 已提交
602
- 通过子类引用父类的静态字段,不会导致子类初始化。
C
CyC2018 已提交
603 604

```java
C
CyC2018 已提交
605
System.out.println(SubClass.value);  // value 字段在 SuperClass 中定义
C
CyC2018 已提交
606 607
```

C
CyC2018 已提交
608
- 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
C
CyC2018 已提交
609

C
CyC2018 已提交
610
```java
C
CyC2018 已提交
611
SuperClass[] sca = new SuperClass[10];
C
CyC2018 已提交
612 613
```

C
CyC2018 已提交
614
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
C
CyC2018 已提交
615 616 617 618

```java
System.out.println(ConstClass.HELLOWORLD);
```
C
CyC2018 已提交
619

C
CyC2018 已提交
620
## 类与类加载器
C
CyC2018 已提交
621

C
CyC2018 已提交
622
两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。
C
CyC2018 已提交
623

C
CyC2018 已提交
624
这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。
C
CyC2018 已提交
625

C
CyC2018 已提交
626
## 类加载器分类
C
CyC2018 已提交
627

C
CyC2018 已提交
628
从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
C
CyC2018 已提交
629

C
CyC2018 已提交
630
- 启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分;
C
CyC2018 已提交
631

C
CyC2018 已提交
632
- 所有其它类的加载器,使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader。
C
CyC2018 已提交
633

C
CyC2018 已提交
634
从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
C
CyC2018 已提交
635

C
CyC2018 已提交
636
- 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 &lt;JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
C
CyC2018 已提交
637

C
CyC2018 已提交
638
- 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 &lt;JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
C
CyC2018 已提交
639

C
CyC2018 已提交
640
- 应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
C
CyC2018 已提交
641

C
CyC2018 已提交
642 643
<div data="modify <--"></div>

C
CyC2018 已提交
644
## 双亲委派模型
C
CyC2018 已提交
645

C
CyC2018 已提交
646
应用程序是由三种类加载器互相配合从而实现类加载,除此之外还可以加入自己定义的类加载器。
C
CyC2018 已提交
647

C
CyC2018 已提交
648
下图展示了类加载器之间的层次关系,称为双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。类加载器之间的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。
C
CyC2018 已提交
649

C
CyC2018 已提交
650
<div align="center"> <img src="pics/805812fa-6ab5-4b8f-a0aa-3bdcadaa829d.png"/> </div><br>
C
CyC2018 已提交
651

C
CyC2018 已提交
652
### 1. 工作过程
C
CyC2018 已提交
653

C
CyC2018 已提交
654
一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。
C
CyC2018 已提交
655

C
CyC2018 已提交
656
### 2. 好处
C
CyC2018 已提交
657

C
CyC2018 已提交
658
使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。
C
CyC2018 已提交
659

C
CyC2018 已提交
660
例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。
C
CyC2018 已提交
661

C
CyC2018 已提交
662
### 3. 实现
C
CyC2018 已提交
663

C
CyC2018 已提交
664
以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。
C
CyC2018 已提交
665

C
CyC2018 已提交
666
```java
C
CyC2018 已提交
667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706
public abstract class ClassLoader {
    // The parent class loader for delegation
    private final ClassLoader parent;

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
C
CyC2018 已提交
707 708 709
}
```

C
CyC2018 已提交
710
## 自定义类加载器实现
C
CyC2018 已提交
711

C
CyC2018 已提交
712
FileSystemClassLoader 是自定义类加载器,继承自 java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。
C
CyC2018 已提交
713

C
CyC2018 已提交
714
java.lang.ClassLoader 的 loadClass() 实现了双亲委派模型的逻辑,自定义类加载器一般不去重写它,但是需要重写 findClass() 方法。
C
CyC2018 已提交
715 716

```java
C
CyC2018 已提交
717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755
public class FileSystemClassLoader extends ClassLoader {

    private String rootDir;

    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }
C
CyC2018 已提交
756 757 758
}
```

C
CyC2018 已提交
759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774
# 参考资料

- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
- [Chapter 2. The Structure of the Java Virtual Machine](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4)
- [Jvm memory](https://www.slideshare.net/benewu/jvm-memory)
[Getting Started with the G1 Garbage Collector](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html)
- [JNI Part1: Java Native Interface Introduction and “Hello World” application](http://electrofriends.com/articles/jni/jni-part1-java-native-interface/)
- [Memory Architecture Of JVM(Runtime Data Areas)](https://hackthejava.wordpress.com/2015/01/09/memory-architecture-by-jvmruntime-data-areas/)
- [JVM Run-Time Data Areas](https://www.programcreek.com/2013/04/jvm-run-time-data-areas/)
- [Android on x86: Java Native Interface and the Android Native Development Kit](http://www.drdobbs.com/architecture-and-design/android-on-x86-java-native-interface-and/240166271)
- [深入理解 JVM(2)——GC 算法与内存分配策略](https://crowhawk.github.io/2017/08/10/jvm_2/)
- [深入理解 JVM(3)——7 种垃圾收集器](https://crowhawk.github.io/2017/08/15/jvm_3/)
- [JVM Internals](http://blog.jamesdbloom.com/JVMInternals.html)
- [深入探讨 Java 类加载器](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code6)
- [Guide to WeakHashMap in Java](http://www.baeldung.com/java-weakhashmap)
- [Tomcat example source code file (ConcurrentCache.java)](https://alvinalexander.com/java/jwarehouse/apache-tomcat-6.0.16/java/org/apache/el/util/ConcurrentCache.java.shtml)