# 垃圾收集器

主要是介绍不同收集器得优劣以及使用

  • 单线程与多线程:单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程。

  • 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并形指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。

# Serial 收集器

Serial 翻译为串行,即以串行的方式执行。它是单线程的收集器,只会使用一个线程进行垃圾收集工作。在进行垃圾收集时,其他线程都要暂停等待。尽管缺点明显,但是从 JDK1.3.1 开始,Serial 收集器就是默认的垃圾收集器了(客户端模式)。

优点:

  • 设计简单高效。
  • 在用户的桌面应用场景中,内存一般不大,可以在较短时间内完成垃圾收集,只要不频繁发生,使用串行回收器是可以接受的。

客户端模式一般是一些桌面级图形化界面应用程序,使用内存较少。

我们可以修改 JDK 当前模式为客户端模式,可以在 jvm.cfg 文件将内容修改为

-client KNOWN
-server IGNORE

使用 java -version 查看当前模式。(下图还没有修改为客户端模式)

它是 Client 模式下的默认新生代收集器,因为在用户的桌面应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。

# ParNew 收集器

是 Serial 收集器的多线程版本。

是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。

默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。

# Parallel Scavenge 收集器

其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为 “吞吐量优先” 收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。

  • 停顿时间短适合用于与用户交互的程序,良好的响应速度提升用户体验
  • 高吞吐量可以尽快完成程序运算任务,适合后台运算而不需要太多交互的任务。

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

还有个 Parallel Old 收集器,是 Parallel Scavenge 老年代版本,在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。

# Serial Old 收集器

Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。

如果用在 Server 模式下,它有两大用途:

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

# CMS 收集器

CMS (Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。

这款收集器是 HotSpot 虚拟机中第一款真正意义上的并发(注意这里的并发和之前的并行是有区别的,并发可以理解为同时运行用户线程和 GC 线程,而并行可以理解为多条 GC 线程同时工作)收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。

整个流程分为四个阶段:

  • 初始标记(需要暂停用户线程):这个阶段的主要任务仅仅只是标记出 GC Roots 能直接关联到的对象,速度比较快,不用担心会停顿太长时间。

直接关联:虚拟机栈,本地方法栈,被锁的对象,静态引用(方法区),虚拟机内部使用的对象

  • 并发标记:从 GC Roots 的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。

  • 重新标记(需要暂停用户线程):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。这个时间会比初始标记时间长一丢丢。

  • 并发清除:最后就可以直接将所有标记好的无用对象进行删除,因为这些对象程序中也用不到了,所以可以与用户线程并发运行。

具有以下缺点:

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

# G1 收集器

堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1(Garbage First ) 可以直接对新生代和老年代一起回收。

垃圾回收分为 Minor GCMajor GCFull GC ,它们分别对应的是新生代,老年代和整个堆内存的垃圾回收,而 G1 收集器巧妙地绕过了这些约定,它将整个 Java 堆划分成 2048 个大小相同的独立 Region 块,每个 Region块 的大小根据堆空间的实际大小而定,整体被控制在 1MB 到 32MB 之间,且都为 2 的 N 次幂。所有的 Region 大小相同,且在 JVM 的整个生命周期内不会发生改变。

每一个 Region 都可以根据需要,自由决定扮演哪个角色(Eden、Survivor 和老年代),收集器会根据对应的角色采用不同的回收策略。此外,G1 收集器还存在一个 Humongous 区域,它专门用于存放大对象(一般认为大小超过了 Region 容量一半的对象为大对象)这样,新生代、老年代在物理上,不再是一个连续的内存区域,而是到处分布的。

回收过程与 CMS 相似:

  • 初始标记(暂停用户线程):仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region 中分配新对象。

  • 并发标记:从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。

  • 最终标记(暂停用户线程):对用户线程做一个短暂的暂停,用于处理并发标记阶段漏标的那部分对象。

  • 筛选回收:负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多个收集器线程并行完成的。

# 参考

https://www.yuque.com/qingkongxiaguang/javase/hla7hr#2466c792

https://pdai.tech/md/java/jvm/java-jvm-gc.html# 垃圾收集器