JVM 垃圾收集器

142 阅读6分钟

Java 有哪些垃圾收集器

image.png (两者之间存在连线则代表两个GC收集器可以搭配使用)

在上图中共有十款GC收集器,它们可以根据回收时的属性分为分代和分区两种类型:

分代收集器:Serial、ParNew、Parallel Scavenge、CMS、Serial Old(MSC)、Parallel Old

分区收集器:G1、ZGC、Shenandoah

其中Epsilon是个例外,这款收集器是JDK11提供的,这款GC收集器俗称为“废物收集器”,装载该收集器的Java程序,在运行期间不会发生任何GC相关的操作,程序所分配的堆空间一旦用完,Java程序就会因OOM原因退出。Epsilon收集器主要是用于程序上线前做测试使用,如:性能测试、内存压力测试、VM接口测试等。在程序启动时选择装载Epsilon收集器,这样可以帮助我们过滤掉GC机制引起的性能假象

分代收集器

新生代收集器

  1. Serial 收集器

    1. 最原始的收集器,基于单线程工作
    2. 在发生垃圾收集时会产生SWT
    3. 采用复制算法
    4. 在单核情况下,它的效果很好
  2. ParNew 收集器

    1. 可以称之为Serial 的多线程版本
    2. 一定程度上降低了 STW 的时间
    3. 除开多线程之外,其他均与Serial 相同
  3. Parallel Scavenge 收集器

    1. 大体上的特征与ParNew 非常像

    2. 唯一的不同就是它们的侧重点,ParNew 是侧重于减少 STW 的时间

    3. 但是 PS ,它是更加专注于程序运行的吞吐量(也就是提高/保持CPU的利用率)

这里比较难区分,我引用知乎老哥的结论:

Parallel Scavenge 尽量减少GC次数,每次GC时间长一些,但吞吐量较高。

ParNew进行频繁的GC,每次GC时间短一些,停顿时间短,但吞吐量相对偏低

老年代收集器

  1. Serial Old 收集器(MSC)

    1. 工作方式同Serial ,是一款单线程的收集器
    2. 但是它使用的是 标记-整理算法
  2. Paraller Old 收集器

    1. 是 PS 的老年代版本,同样采用多线程,注重吞吐量
    2. 但是在回收算法使用的是 标记-整理算法
  3. CMS 收集器

    1. 全称为 ConcurrentMarkSweep

    2. 首款使用了并发收集的回收器(就是不停止程序,GC 线程和用户线程一起工作

    3. 它追求最短回收时间,使用的是标记-清除算法

    4. 回收过程

      1. 初始标记(STW) :主要是标记 GC Root 开始的下级(注:仅下一级)对象,这个过程会 STW,但是跟 GC Root 直接关联的下级对象不会很多,因此这个过程其实很快。

      2. 并发标记:根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。该阶段的GC线程会与用户线程同时执行

      3. 重新标记(STW):顾名思义,就是要再标记一次。因为第 2 步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾,该阶段需要在STW中执行,并且该阶段的停顿时间会比初始阶段要长不少。

      4. 并发清除:清除阶段是清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象(标记清除算法),所以这个阶段也是可以与用户线程同时并发进行的

image.png

总结

垃圾回收器类型回收算法线程工作方式作用区域
Serial复制算法单线程年轻代
ParNew复制算法多线程年轻代
Parallel Scavenge复制算法多线程年轻代
MSC标记-整理算法单线程老年代
Parallel Old标记-整理算法多线程老年代
CMS标记-清除算法多线程老年代

从关注度来看,又可分为吞吐量优先、响应时间优先两大类。 一般而言,如果你的程序是更为关注用户体验度,那么可以采用响应速度优先的收集器工作,因为该类收集器造成的程序暂停不会很久。 但如若你的程序不需要与用户有特别多的交互,如批量处理、订单处理、报表计算、科学计算等类型的后台系统,那你则可以采用吞吐量优先的收集器,因为高吞吐量可以高效率地利用CPU资源

JDK8 默认的组合是 Parallel Scavenge + Parallel Old,但是JDK9默认是使用下面的G1收集器

分区收集器

G1收集器

G1(Garbage First)回收器采用面向局部收集的设计思路和基于Region的内存布局形式,是一款主要面向服务端应用的垃圾回收器。

空间结构:

image.png G1将Java堆划分为多个大小相等的独立的Region区域,G1收集器虽然在逻辑上存在分代的概念,但不再是物理隔阂了,也就是指在物理内存上是不分代的,内存空间会被划分为一个个的Region区,这样做的好处在于:JVM不需要再为堆空间分配连续的内存,堆空间可以是不连续物理内存来组成Region的集合

Humongous区存在的意义:可以避免一些“短命”的巨型对象直接进入年老代,节约年老代的内存空间,可以有效避免年老代因空间不足时的GC开销

G1设计初衷就是替换 CMS,成为一种全功能收集器。G1 在JDK9 之后成为服务端模式下的默认垃圾回收器,取代了 Parallel Scavenge 加 Parallel Old 的默认组合,而 CMS 被声明为不推荐使用的垃圾回收器。G1从整体来看是基于 标记-整理 算法实现的回收器,但从局部(两个Region之间)上看又是基于 标记-复制 算法实现的

回收过程:

  1. 初始标记:先触发STW,然后使用单条GC线程快速标记GCRoots直连的对象。
  2. 并发标记:与CMS的并发标记过程一致,采用多条GC线程与用户线程共同执行,根据Root根节点标记所有对象。
  3. 最终标记:同CMS的重新标记阶段,主要是为了纠正并发标记阶段因用户操作导致的错标、误标、漏标对象。
  4. 筛选回收:先对各个Region区的回收价值和成本进行排序,找出回收价值最大的Region优先回收

image.png

G1收集器正是由于「筛选回收」阶段的存在,所以才得以冠名「垃圾优先收集器」。在该阶段中,对各个Region区排序后,G1会根据用户指定的期望停顿时间(即-XX:MaxGCPauseMillis参数设定的值)选择「价值最大且最符合用户预期」的Region区进行回收

参考资料:JVM成神路 - 竹子爱熊猫的专栏 - 掘金 (juejin.cn)