1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-06-16 18:10:13 +08:00

[docs update]jvm垃圾回收部分内容精简

This commit is contained in:
guide 2022-07-14 21:08:05 +08:00
parent 80d9e98cb6
commit be2dd381a4
2 changed files with 83 additions and 185 deletions

View File

@ -5,168 +5,61 @@ tag:
- JVM
---
## 写在前面
> 如果没有特殊说明,都是针对的是 HotSpot 虚拟机。
>
> 本文基于《深入理解 Java 虚拟机JVM 高级特性与最佳实践》进行总结补充。
>
> 常见面试题
>
> - 如何判断对象是否死亡(两种方法)。
> - 简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好处)。
> - 如何判断一个常量是废弃常量
> - 如何判断一个类是无用的类
> - 垃圾收集有哪些算法,各自的特点?
> - HotSpot 为什么要分为新生代和老年代?
> - 常见的垃圾回收器有哪些?
> - 介绍一下 CMS,G1 收集器。
> - Minor Gc 和 Full GC 有什么不同呢?
### 本节常见面试题
问题答案在文中都有提到
- 如何判断对象是否死亡(两种方法)。
- 简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好处)。
- 如何判断一个常量是废弃常量
- 如何判断一个类是无用的类
- 垃圾收集有哪些算法,各自的特点?
- HotSpot 为什么要分为新生代和老年代?
- 常见的垃圾回收器有哪些?
- 介绍一下 CMS,G1 收集器。
- Minor Gc 和 Full GC 有什么不同呢?
### 本文导火索
![](./pictures/jvm垃圾回收/29176325.png)
## 前言
当需要排查各种内存溢出问题、当垃圾收集成为系统达到更高并发的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。
## 1 揭开 JVM 内存分配与回收的神秘面纱
## 堆空间的基本结构
Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时Java 自动内存管理最核心的功能是 **堆** 内存中对象的分配与回收。
Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆Garbage Collected Heap**.从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为新生代和老年代再细致一点有Eden 空间、From Survivor、To Survivor 空间等。**进一步划分的目的是更好地回收内存,或者更快地分配内存。**
Java 堆是垃圾收集器管理的主要区域,因此也被称作 **GC 堆Garbage Collected Heap**
**堆空间的基本结构:**
从垃圾回收的角度来说,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆被划分为了几个不同的区域,这样我们就可以根据各个区域的特点选择合适的垃圾收集算法。
![](./pictures/jvm垃圾回收/01d330d8-2710-4fad-a91c-7bbbfaaefc0e.png)
在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:
上图所示的 Eden 区、From Survivor0("From") 区、To Survivor1("To") 区都属于新生代Old Memory 区属于老年代。
1. 新生代内存(Young Generation)
2. 老生代(Old Generation)
3. 永久代(Permanent Generation)
大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为大于 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置默认值,这个值会在虚拟机运行过程中进行调整,可以通过`-XX:+PrintTenuringDistribution`来打印出当次 GC 后的 Threshold。
下图所示的 Eden 区、两个 Survivor 区 S0 和 S1 都属于新生代,中间一层属于老年代,最下面一层属于永久代
> **🐛 修正(参见:[issue552](https://github.com/Snailclimb/JavaGuide/issues/552)**“Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值”。
>
> **动态年龄计算的代码如下**
>
> ```c++
> uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
> //survivor_capacity是survivor空间的大小
> size_t desired_survivor_size = (size_t)((((double)survivor_capacity)*TargetSurvivorRatio)/100);
> size_t total = 0;
> uint age = 1;
> while (age < table_size) {
> //sizes数组是每个年龄段对象大小
> total += sizes[age];
> if (total > desired_survivor_size) {
> break;
> }
> age++;
> }
> uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
> ...
> }
>
> ```
![hotspot-heap-structure](./pictures/java内存区域/hotspot-heap-structure.png)
经过这次 GC 后Eden 区和"From"区已经被清空。这个时候,"From"和"To"会交换他们的角色,也就是新的"To"就是上次 GC 前的“From”新的"From"就是上次 GC 前的"To"。不管怎样,都会保证名为 To 的 Survivor 区域是空的。Minor GC 会一直重复这样的过程,在这个过程中,有可能当次 Minor GC 后Survivor 的"From"区域空间不够用,有一些还达不到进入老年代条件的实例放不下,则放不下的部分会提前进入老年代
**JDK 8 版本之后 PermGen(永久) 已被 Metaspace(元空间) 取代,元空间使用的是直接内存** 。
接下来我们提供一个调试脚本来测试这个过程
关于堆空间结构更详细的介绍,可以回过头看看 [Java 内存区域详解](./memory-area.md) 这篇文章。
**调试代码参数如下**
## 内存分配和回收原则
```
-verbose:gc
-Xmx200M
-Xms200M
-Xmn50M
-XX:+PrintGCDetails
-XX:TargetSurvivorRatio=60
-XX:+PrintTenuringDistribution
-XX:+PrintGCDateStamps
-XX:MaxTenuringThreshold=3
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
```
### 对象优先在 Eden 区分配
**示例代码如下:**
大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。下面我们来进行实际测试以下。
```java
/*
* 本实例用于java GC以后新生代survivor区域的变化以及晋升到老年代的时间和方式的测试代码。需要自行分步注释不需要的代码进行反复测试对比
*
* 由于java的main函数以及其他基础服务也会占用一些eden空间所以要提前空跑一次main函数来看看这部分占用。
*
* 自定义的代码中我们使用堆内分配数组和栈内分配数组的方式来分别模拟不可被GC的和可被GC的资源。
*
*
* */
public class JavaGcTest {
public static void main(String[] args) throws InterruptedException {
//空跑一次main函数来查看java服务本身占用的空间大小我这里是占用了3M。所以40-3=37下面分配三个1M的数组和一个34M的垃圾数组。
// 为了达到TargetSurvivorRatio期望占用的Survivor区域的大小这个比例指定的值, 即5M*60%=3M(Desired survivor size)
// 这里用1M的数组的分配来达到Desired survivor size
//说明: 5M为S区的From或To的大小60%为TargetSurvivorRatio参数指定,可以更改参数获取不同的效果。
byte[] byte1m_1 = new byte[1 * 1024 * 1024];
byte[] byte1m_2 = new byte[1 * 1024 * 1024];
byte[] byte1m_3 = new byte[1 * 1024 * 1024];
//使用函数方式来申请空间函数运行完毕以后就会变成垃圾等待回收。此时应保证eden的区域占用达到100%。可以通过调整传入值来达到效果。
makeGarbage(34);
//再次申请一个数组因为eden已经满了所以这里会触发Minor GC
byte[] byteArr = new byte[10*1024*1024];
// 这次Minor Gc时, 三个1M的数组因为尚有引用所以进入From区域因为是第一次GCage为1
// 且由于From区已经占用达到了60%(-XX:TargetSurvivorRatio=60), 所以会重新计算对象晋升的age。
// 计算方法见上文计算出agemin(age, MaxTenuringThreshold) = 1输出中会有Desired survivor size 3145728 bytes, new threshold 1 (max 3)字样
//新的数组byteArr进入eden区域。
//再次触发垃圾回收证明三个1M的数组会因为其第二次回收后age为2大于上一次计算出的new threshold 1所以进入老年代。
//而byteArr因为超过survivor的单个区域直接进入了老年代。
makeGarbage(34);
}
private static void makeGarbage(int size){
byte[] byteArrTemp = new byte[size * 1024 * 1024];
}
}
```
注意:如下输出结果中老年代的信息为 `concurrent mark-sweep generation` 和以前版本略有不同。另外,还列出了某次 GC 后是否重新生成了 threshold以及各个年龄占用空间的大小。
```bash
2021-07-01T10:41:32.257+0800: [GC (Allocation Failure) 2021-07-01T10:41:32.257+0800: [ParNew
Desired survivor size 3145728 bytes, new threshold 1 (max 3)
- age 1: 3739264 bytes, 3739264 total
: 40345K->3674K(46080K), 0.0014584 secs] 40345K->3674K(199680K), 0.0015063 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2021-07-01T10:41:32.259+0800: [GC (Allocation Failure) 2021-07-01T10:41:32.259+0800: [ParNew
Desired survivor size 3145728 bytes, new threshold 3 (max 3)
: 13914K->0K(46080K), 0.0046596 secs] 13914K->13895K(199680K), 0.0046873 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
par new generation total 46080K, used 35225K [0x05000000, 0x08200000, 0x08200000)
eden space 40960K, 86% used [0x05000000, 0x072667f0, 0x07800000)
from space 5120K, 0% used [0x07800000, 0x07800000, 0x07d00000)
to space 5120K, 0% used [0x07d00000, 0x07d00000, 0x08200000)
concurrent mark-sweep generation total 153600K, used 13895K [0x08200000, 0x11800000, 0x11800000)
Metaspace used 153K, capacity 2280K, committed 2368K, reserved 4480K
```
### 1.1 对象优先在 eden 区分配
目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC.下面我们来进行实际测试以下。
**测试:**
测试代码:
```java
public class GCTest {
public static void main(String[] args) {
byte[] allocation1, allocation2;
allocation1 = new byte[30900*1024];
//allocation2 = new byte[900*1024];
}
}
```
@ -181,7 +74,9 @@ public class GCTest {
![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-26/28954286.jpg)
从上图我们可以看出 eden 区内存几乎已经被分配完全(即使程序什么也不做,新生代也会使用 2000 多 k 内存)。假如我们再为 allocation2 分配内存会出现什么情况呢?
从上图我们可以看出 Eden 区内存几乎已经被分配完全(即使程序什么也不做,新生代也会使用 2000 多 k 内存)。
假如我们再为 `allocation2` 分配内存会出现什么情况呢?
```java
allocation2 = new byte[900*1024];
@ -189,7 +84,9 @@ allocation2 = new byte[900*1024];
![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-26/28128785.jpg)
**简单解释一下为什么会出现这种情况:** 因为给 allocation2 分配内存的时候 eden 区内存几乎已经被分配完了,我们刚刚讲了当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC.GC 期间虚拟机又发现 allocation1 无法存入 Survivor 空间,所以只好通过 **分配担保机制** 把新生代的对象提前转移到老年代中去,老年代上的空间足够存放 allocation1所以不会出现 Full GC。执行 Minor GC 后,后面分配的对象如果能够存在 eden 区的话,还是会在 eden 区分配内存。可以执行如下代码验证:
`allocation2` 分配内存的时候 Eden 区内存几乎已经被分配完了
当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。GC 期间虚拟机又发现 `allocation1` 无法存入 Survivor 空间,所以只好通过 **分配担保机制** 把新生代的对象提前转移到老年代中去,老年代上的空间足够存放 `allocation1`,所以不会出现 Full GC。执行 Minor GC 后,后面分配的对象如果能够存在 Eden 区的话,还是会在 Eden 区分配内存。可以执行如下代码验证:
```java
public class GCTest {
@ -206,23 +103,19 @@ public class GCTest {
```
### 1.2 大对象直接进入老年代
### 大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
**为什么要这样呢?**
大对象直接进入老年代主要是为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
### 1.3 长期存活的对象将进入老年代
### 长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存那么内存回收时就必须能识别哪些对象应放在新生代哪些对象应放在老年代中。为了做到这一点虚拟机给每个对象一个对象年龄Age计数器。
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置
大部分情况,对象都会首先在 Eden 区域分配。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间s0 或者 s1并将对象年龄设为 1(Eden 区->Survivor 区后对象的初始年龄变为 1)
### 1.4 动态对象年龄判定
大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。
对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。
> 修正([issue552](https://github.com/Snailclimb/JavaGuide/issues/552)“Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的 50% 时(默认值是 50%,可以通过 `-XX:TargetSurvivorRatio=percent` 来设置,参见 [issue1199](https://github.com/Snailclimb/JavaGuide/issues/1199) ),取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值”。
>
@ -242,7 +135,7 @@ public class GCTest {
> //sizes数组是每个年龄段对象大小
> total += sizes[age];
> if (total > desired_survivor_size) {
> break;
> break;
> }
> age++;
> }
@ -257,7 +150,7 @@ public class GCTest {
>
> **Sets the maximum tenuring threshold for use in adaptive GC sizing. The largest value is 15. The default value is 15 for the parallel (throughput) collector, and 6 for the CMS collector.默认晋升年龄并不都是 15这个是要区分垃圾收集器的CMS 就是 6.**
### 1.5 主要进行 gc 的区域
### 主要进行 gc 的区域
周志明先生在《深入理解 Java 虚拟机》第二版中 P92 如是写道:
@ -279,7 +172,7 @@ public class GCTest {
整堆收集 (Full GC):收集整个 Java 堆和方法区。
### 1.6 空间分配担保
### 空间分配担保
空间分配担保是为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间。
@ -289,32 +182,37 @@ public class GCTest {
>
> JDK 6 Update 24 之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC否则将进行 Full GC。
## 2 对象已经死亡?
## 死亡对象判断方法
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。
### 2.1 引用计数法
### 引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1当引用失效计数器就减 1任何时候计数器为 0 的对象就是不可能再被使用的。
给对象中添加一个引用计数器
**这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。** 所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0于是引用计数算法无法通知 GC 回收器回收他们。
- 每当有一个地方引用它,计数器就加 1
- 当引用失效,计数器就减 1
- 任何时候计数器为 0 的对象就是不可能再被使用的。
**这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。**
所谓对象之间的相互引用问题,如下面代码所示:除了对象 `objA``objB` 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0于是引用计数算法无法通知 GC 回收器回收他们。
```java
public class ReferenceCountingGc {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}
```
### 2.2 可达性分析算法
### 可达性分析算法
这个算法的基本思想就是通过一系列的称为 **“GC Roots”** 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
@ -343,7 +241,7 @@ public class ReferenceCountingGc {
> - [JEP 421: Deprecate Finalization for Removal](https://openjdk.java.net/jeps/421)
> - [是时候忘掉 finalize 方法了](https://mp.weixin.qq.com/s/LW-paZAMD08DP_3-XCUxmg)
### 2.3 再谈引用
### 引用类型总结
无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。
@ -377,7 +275,7 @@ JDK1.2 以后Java 对引用的概念进行了扩充,将引用分为强引
特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为**软引用可以加速 JVM 对垃圾内存的回收速度可以维护系统的运行安全防止内存溢出OutOfMemory等问题的产生**。
### 2.5 如何判断一个常量是废弃常量?
### 如何判断一个常量是废弃常量?
运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?
@ -391,7 +289,7 @@ JDK1.2 以后Java 对引用的概念进行了扩充,将引用分为强引
假如在字符串常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池了。
### 2.6 如何判断一个类是无用的类
### 如何判断一个类是无用的类
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
@ -403,9 +301,9 @@ JDK1.2 以后Java 对引用的概念进行了扩充,将引用分为强引
虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。
## 3 垃圾收集算法
## 垃圾收集算法
### 3.1 标记-清除算法
### 标记-清除算法
该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:
@ -414,19 +312,19 @@ JDK1.2 以后Java 对引用的概念进行了扩充,将引用分为强引
![](./pictures/jvm垃圾回收/标记-清除算法.jpeg)
### 3.2 标记-复制算法
### 标记-复制算法
为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
![复制算法](./pictures/jvm垃圾回收/90984624.png)
### 3.3 标记-整理算法
### 标记-整理算法
根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
![标记-整理算法 ](./pictures/jvm垃圾回收/94057049.png)
### 3.4 分代收集算法
### 分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
@ -436,13 +334,13 @@ JDK1.2 以后Java 对引用的概念进行了扩充,将引用分为强引
根据上面的对分代收集算法的介绍回答。
## 4 垃圾收集器
## 垃圾收集器
**如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。**
虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,**我们能做的就是根据具体应用场景选择适合自己的垃圾收集器**。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的 HotSpot 虚拟机就不会实现那么多不同的垃圾收集器了。
### 4.1 Serial 收集器
### Serial 收集器
Serial串行收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 **“单线程”** 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( **"Stop The World"** ),直到它收集结束。
@ -454,7 +352,7 @@ Serial串行收集器是最基本、历史最悠久的垃圾收集器了
但是 Serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它**简单而高效(与其他收集器的单线程相比)**。Serial 收集器由于没有线程交互的开销自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。
### 4.2 ParNew 收集器
### ParNew 收集器
**ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。**
@ -470,7 +368,7 @@ Serial串行收集器是最基本、历史最悠久的垃圾收集器了
- **并发Concurrent**:指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。
### 4.3 Parallel Scavenge 收集器
### Parallel Scavenge 收集器
Parallel Scavenge 收集器也是使用标记-复制算法的多线程收集器,它看上去几乎和 ParNew 都一样。 **那么它有什么特别之处呢?**
@ -493,9 +391,9 @@ Parallel Scavenge 收集器也是使用标记-复制算法的多线程收集器
**这是 JDK1.8 默认收集器**
使用 java -XX:+PrintCommandLineFlags -version 命令查看
使用 `java -XX:+PrintCommandLineFlags -version` 命令查看
```
```bash
-XX:InitialHeapSize=262921408 -XX:MaxHeapSize=4206742528 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
@ -504,15 +402,15 @@ Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)
JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old如果指定了-XX:+UseParallelGC 参数,则默认指定了-XX:+UseParallelOldGC可以使用-XX:-UseParallelOldGC 来禁用该功能
### 4.4.Serial Old 收集器
### Serial Old 收集器
**Serial 收集器的老年代版本**,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。
### 4.5 Parallel Old 收集器
### Parallel Old 收集器
**Parallel Scavenge 收集器的老年代版本**。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
### 4.6 CMS 收集器
### CMS 收集器
**CMSConcurrent Mark Sweep收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。**
@ -533,7 +431,7 @@ JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old如果指定了-XX:
- **无法处理浮动垃圾;**
- **它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。**
### 4.7 G1 收集器
### G1 收集器
**G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.**
@ -553,7 +451,7 @@ G1 收集器的运作大致分为以下几个步骤:
**G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)** 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
### 4.8 ZGC 收集器
### ZGC 收集器
与 CMS 中的 ParNew 和 G1 类似ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB