JVM04——七个GC垃圾收集器
Java 堆内存被划分为新生代和老年代两部分,因此 JVM 通常采用分代回收算法。新生代主要使用复制和标记-清除垃圾回收算法 ,老年代主要使用标记-整理垃圾回收算法。JVM 中针对新生代和年老代分别提供了多种不同的垃圾收集器。
根据线程特点,可以将收集器分为三类:
- 串行收集器:Serial 收集器、Serial Old 收集器;
- 并行收集器:Parallel Scavenge 收集器、Parallel Old 收集器、ParNew 收集器;
- 并发收集器:CMS 收集器、G1 收集器。
1|0Serial 收集器
- 新生代:Serial 垃圾收集器是 JVM 运行在 Client 模式下默认的新生代垃圾收集器;
- 复制算法 :Serial 是最基本垃圾收集器,使用复制算法,曾经是 JDK1.3.1 之前新生代唯一的垃圾收集器;
- 单线程:Serial 只会使用一个 CPU 或一条线程去完成垃圾收集工作;
- 线程暂停:Serial 在进行垃圾收集的时,必须暂停其他所有的工作线程,直到垃圾收集结束;
- 单线程效率最高:Serial 对于限定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率。
2|0ParNew 收集器
- 新生代:ParNew 垃圾收集器是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器;
- 复制算法 : ParNew 和 Serial 一样使用了复制算法;
- 多线程 :ParNew 垃圾收集器其实是 Serial 收集器的多线程版本;
- 线程暂停:ParNew 和 Serial 一样垃圾收集的同时,必须暂停其他所有的工作线程;
- 默认线程数:ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。
3|0Parallel Scavenge 收集器
- 新生代:Parallel Scavenge 收集器也是一个新生代垃圾收集器;
- 复制算法:Parallel Scavenge 收集器同样使用复制算法;
- 多线程 :Parallel Scavenge 收集器也是一个多线程的垃圾收集器;
- 高吞吐量:Parallel Scavenge 重点关注的是程序达到一个可控制的吞吐量(Thoughput,吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)),高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务;
- 适用场景:主要适用于在后台运算而不需要太多交互的任务。自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
新生代 Parallel Scavenge 收集器与 ParNew 收集器工作原理类似:
- 都是多线程的收集器;
- 都使用的是复制算法;
- 在垃圾收集过程中都需要暂停所有的工作线程。
4|0Serial Old 收集器
- 老年代:Serial Old 是 Serial 垃圾收集器老年代的版本;
- 标记-整理算法:Serial Old 使用标记-整理算法;
- 单线程:Serial Old 与 Serial 一样是单线程收集器;
- Client 模式:JVM 运行在 Client 模式下,Serial Old 是默认的老年代垃圾收集器;
- Server 模式:JVM 运行在 Server 模式下,Serial Old 主要有两个用途:
- 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用;
- 作为年老代中使用 CMS 收集器的后备垃圾收集方案。
新生代 Serial 与年老代 Serial Old 搭配垃圾收集过程图:
新生代 Parallel Scavenge 收集器与 ParNew 收集器工作原理类似,都是多线程的收集器,都使用的是复制算法,在垃圾收集过程中都需要暂停所有的工作线程。新生代 Parallel Scavenge/ParNew 与年老代 Serial Old 搭配垃圾收集过程图:
5|0Parallel Old 收集器
- 老年代:Parallel Old 收集器是 Parallel Scavenge 的老年代版本;
- 标记-整理算法:Parallel Old 收集器使用标记-整理算法;
- 多线程:Parallel Old 收集器是多线程收集器;
- JDK1.6:Parallel Old 是 JDK1.6才开始提供的。
在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略。
新生代 Parallel Scavenge 和年老代 Parallel Old 收集器搭配运行过程图:
6|0CMS 收集器
- 老年代:CMS(Concurrent mark sweep)收集器是一种年老代垃圾收集器;
- 标记-清理算法:和其他年老代使用标记-整理算法,CMS 使用标记-清除算法;
- 多线程 :CMS 采用的是多线程的标记-清除算法;
- 停顿时间端: CMS最主要目标是获取最短垃圾回收停顿时间,最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。
CMS 运行过程分为以下 4 个阶段:
- 初始标记:标记 GC Roots 能直接关联的对象(速度很快,需要暂停所有的工作线程);
- 并发标记:进行 GC Roots 跟踪(和用户线程一起工作,不需要暂停工作线程);
- 重新标记:修正因用户程序继续运行而导致标记产生变动的那一部分对象的标记(需要暂停所有的工作线程);
- 并发清除:清除 GC Roots 不可达对象(和用户线程一起工作,不需要暂停工作线程)。
由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。
7|0G1 收集器
7|1特点
- 无分代:G1 将新生代,老年代的物理空间划分取消了。这样我们再也不用单独的空间对每个代进行设置了,不用担心每个代内存是否足够;
- 标记-整理算法:G1 收集器采用标记-整理算法,无内存碎片产生;
- 分区回收:G1 虽然没有了新生代与老年代的物理限制,但是 G1 采取内存分区策略,将堆内存划分为大小固定的几个独立区域。在分区中,同时存在新生代与老年代;
7|2分区
新生代区域:G1 收集器中新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者 Survivor 空间;
老年代区域:G1 收集器通过将对象从一个区域复制到另外一个区域,以此来完成老年代的清理工作;
Humongous区域:巨型对象区域。如果一个对象占用的空间超过了分区容量 50% 以上,G1 收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1 划分了一个 Humongous 区,它用来专门存放巨型对象。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储。为了能找到连续的 H 区,有时候不得不启动 Full GC。
7|3对象分配策略
说起大对象的分配,我们不得不谈谈对象的分配策略。它分为3个阶段:
- TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区
- Eden区中分配
- Humongous区分配
TLAB为线程本地分配缓冲区,它的目的为了使对象尽可能快的分配出来。如果对象在一个共享的空间中分配,我们需要采用一些同步机制来管理这些空间内的空闲空间指针。在Eden空间中,每一个线程都有一个固定的分区用于分配对象,即一个TLAB。分配对象时,线程之间不再需要进行任何的同步。
对TLAB空间中无法分配的对象,JVM会尝试在Eden空间中进行分配。如果Eden空间无法容纳该对象,就只能在老年代中进行分配空间。
最后,G1提供了两种GC模式,Young GC和Mixed GC,两种都是Stop The World(STW)的。下面我们将分别介绍一下这2种模式。
7|4G1 Young GC
Young GC 主要是对 Eden 区进行 GC ,它在 Eden 空间耗尽时会被触发。在这种情况下,Eden 空间的数据移动到 Survivor 空间中,如果 Survivor 空间不够,Eden 空间的部分数据会直接晋升到年老代空间。Survivor 区的数据移动到新的 Survivor 区中,也有部分数据晋升到老年代空间中。最终 Eden 空间的数据为空,GC 停止工作,应用线程继续执行。
G1 Young GC 阶段:
- 根扫描:静态和本地对象被扫描;
- 更新RS: 处理 Dirty Card 队列更新 RS(Remembered Set,作用是跟踪指向某个 Heap 区内的对象引用);
- 处理RS: 检测从年轻代指向年老代的对象;
- 对象拷贝:拷贝存活的对象到 Survivor/Old 区域;
- 处理引用队列: 软引用,弱引用,虚引用处理。
7|5G1 Mix GC
Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。
G1 Mix GC运行步骤:
-
全局并发标记(global concurrent marking)
1.1. 初始标记(initial mark,STW):在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关;
1.2. 根区域扫描(root region scan):G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收;
1.3. 并发标记(Concurrent Marking):G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断;
1.4. 最终标记(Remark,STW): 该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理;
1.5. 清除垃圾(Cleanup,STW):在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。 -
拷贝存活对象(evacuation)
G1 收集器与 CMS 收集器相比,G1 收集器两个最突出的改进是:
- 基于标记-整理算法,不产生内存碎片;
- 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
// G1 收集器参数设置 -XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
// -XX:+UseG1GC —— 为开启G1垃圾收集器,
// -Xmx32g —— 设计堆内存的最大内存为32G,
// -XX:MaxGCPauseMillis=200 —— 设置GC的最大暂停时间为200ms