先来看一张图,串行代表两个垃圾回收器按顺序执行,并行代表同时执行。STW代表工作线程暂停,Stop The World的意思。
垃圾回收器 | 执行顺序 | 执行方式 | 作用区域 | 使用算法 | 说明 |
---|---|---|---|---|---|
Serial GC | 串行 | 工作线程暂停,单线程进行垃圾回收 | 新生代 | 复制算法 | |
Serial Old GC | 串行 | 工作线程暂停,单线程进行垃圾回收 | 老年代 | 标记-整理算法 | |
ParNew GC | 并行 | 工作线程暂停,多线程进行垃圾回收 | 新生代 | 复制算法 | Serial GC的多线程版 |
CMS GC | 并行 | 用户线程和垃圾回收线程同时执行 | 老年代 | 标记-清除算法 | 低暂停 |
Parallel GC | 并行 | 工作线程暂停,多线程进行垃圾回收 | 新生代 | 复制算法 | 和ParNew相比能动态调整内存分配情况JDK8默认 |
Parallel Old GC | 并行 | 工作线程暂停,多线程进行垃圾回收 | 老年代 | 标记整理算法 | 替代串行的Serial Old GC |
G1 | 并行 | 用户线程和垃圾回收线程同时执行 | 整堆 | 分区算法 | 在延迟可控的情况下尽可能提高吞吐量JDK9默认 |
ZGC | 并行 | 用户线程和垃圾回收线程同时执行 | 整堆 | 分页算法 | STW的时间不超过1ms,且不会随着堆的大小增加而增加 |
常用命令:
-XX:+PrintCommandLineFlags,查看使用的垃圾收集器
-XX:+UseSerialGC,指定使用Serial GC处理新生代,并且默认会使用Serial Old GC处理老年代
-XX:+UseParNewGC,指定新生代使用ParNew GC,-XX:+UseConcMarkSweepGC,指定老年代使用CMS GC
-XX:+UseParallelGC,指定新生代使用Parallel GC,-XX:+UserParallelOldGC,指定老年代使用Parallel Old GC,这两个配置一个,另一个自动激活
1. Serial GC、Serial Old GC
Serial GC和Serial Old GC是比较早的垃圾回收器,那个时候一般的服务器CPU还只有一核,所以一个线程进行垃圾回收就够了。串行的意思就是两个垃圾回收器不能同时执行。
2. Parallel GC、Parallel Old GC
Parallel GC进行垃圾回收时也会暂停用户线程(STW),与Serial GC的不同就是它时多线程进行垃圾回收的。下面画个图方便理解垃圾回收过程。
在一次垃圾回收过程中,会进行一次STW,并且会有多个线程同时进行垃圾回收,回收完后用户线程回复运行。
3. CMS GC
CMS全称是ConcMarkSweep,即并发标记清除。但是它不是某个JDK默认的,要使用的话就加上-XX:+UseConcMarkSweepGC参数。
CMS GC是Java9之前我们很常用的垃圾回收器,是针对老年代的使用标记-清除算法的垃圾回收器。我们一般会使用CMS GC替代默认的老年代垃圾回收器,因为CMS GC的特点是低暂停。实现低暂停的方法是在初始标记阶段只标记GC Roots直接可达的对象,这个阶段比较短,所以对用户线程来说是低暂停。
下面用一张图展示它的垃圾回收过程:
阶段一,初始标记:
- STW,暂停所有用户线程。
- 然后标记GC Roots直接可达的对象,就是第一层对象。
- 一旦标记完就恢复用户线程继续执行。
- 这个阶段很快。
阶段二,并发标记:
- 从上一个阶段标记出的对象,开始遍历整个老年代,标记出所有的可达对象。
- 耗时虽然比较长,但好在不需要STW,可与用户线程一起执行。
- 采用的是三色标记算法(大家自行拓展)。
阶段三,重新标记:
- 由于在并发标记阶段,应用程序线程也在运行,可能会导致一些对象的状态发生变化,例如新的对象被创建或旧的对象被删除。
- 为了修正这些问题,需要进行一次短暂的停顿,重新检查并更新标记状态
- 这个阶段也是"Stop-The-World"的,但也不会太长。
阶段四,并发清理:
- 清除垃圾对象,释放空间供后续分配使用。
- 清理工作与应用程序线程并发进行,不会引起停顿。
阶段五,并发重置:
- CMS收集器会准备下一次垃圾回收循环,重置内部数据结构等。
- 这一阶段也是与应用程序并发进行的。
CMS整个垃圾回收过程更长了,但是STW的时间变短了,而且在垃圾回收过程中大部分时间用户线程可以执行,所以用户体验更好了。
在并发清理过程中,可能用户线程会产生新的垃圾,这些就是"浮动垃圾",只能等到下一次GC时来清理。
如果在并发标记、并发清理过程中,用户线程产生的新对象要进入老年代,但是老年代空间又不够,那么就会导致"concurrent mode failure",此时就会利用Serial Old来做一次垃圾回收,就会做一次全局STW。
由于采用的是标记-清除算法,所以会产生内存碎片,可以通过参数 -XX:+UseCMSCompactAtFullCollection 让JVM在执行完标记-清除后再做一次整理,也可以通过 -XX:CMSFullGCsBeforeCompaction 来指定多少次GC后来做整理,默认是0,表示每次GC后都整理。
CMS垃圾回收器能够在大多数时间内与应用程序并发执行,减少了因垃圾回收引起的停顿时间。然而,CMS也有一些缺点,比如它可能产生大量的浮动垃圾(floating garbage),并且在某些情况下仍然会导致较长的停顿。随着Java版本的发展,G1 GC和ZGC等更先进的垃圾回收器逐渐取代了CMS的地位。
4. G1(Garbage-First)
G1垃圾回收器是在 Java 7 update4 之后引入,并在Java 9中成为默认的垃圾收集器的。G1的设计目标是提供高吞吐量的同时,实现可预测的暂停时间,特别适合处理大堆内存的应用场景。下面介绍一些G1垃圾回收器的内存结构。
先看张图:
之前我们讲的垃圾回收器把堆内存分为Eden、S0、S1、老年代这四个区域,每个区域都是连续的。而在G1中,整块堆内存被分成2048个大小相等的region,这些region在物理上不要求是连续的,但在逻辑上是连续的。每个region可以独立地作为Eden、Survivor或老年代的一部分。所以说G1还是把内存分为了Eden、Survivor、老年代区,只不过空间可以是不连续的了。
Humongous区是专门用来存放大对象的(如果一个对象大小超过了一个region的50%,那么就是大对象)
用一张图展示垃圾回收过程:
阶段一,初始标记:
- 短暂的STW。
- 标记出直接可达对象。
阶段二,并发标记:
- 并发遍历整个堆,标记所有可达对象。
- 不需要STW。
- 三色标记。
阶段三,最终标记:
- 这也是一个较短的STW。
- 修正并发标记期间由于应用程序运行而可能产生的误差。
阶段四,筛选回收:
- 需要STW,来清除垃圾对象。
- 可通过 -XX:MaxGCPauseMillis 来指定GC的STW停顿的时间,所以可能并不会回收掉所有的垃圾对象,默认为200ms(略小于人的平均反应速度)。
- 由于回收时间有限,所有会选择更有回收价值(垃圾更多)的区域进行回收。这也是Garbage-First(垃圾优先)的名字由来。
- 采用的是复制算法,不会产生碎片(会把某个region里的可达对象移动到空白的region里)。
由于筛选回收阶段的STW时间可能不够用导致垃圾约积越多,为了避免OOM,G1 中还提供了另外三种垃圾回收模式:Young GC、Mixed GC 和 Full GC,它们有各自的触发条件。
Young GC: Eden区满,就会触发G1的YoungGC,对Eden区进行垃圾回收。
Mixed GC: 老年代的占用率达到了 -XX:InitiatingHeapOccupancyPercent 指定的百分比,回收所有的新生代以及部分老年代,以及大对象区。
Full GC: 在进行Mixed GC的过程中,采用的是复制算法,如果没有空的region可用,就会触发Full GC,会STW,并采用单线程来进行标记-整理算法进行GC,相当于一次Serial GC。