JVM(四)—— JVM垃圾收集器

71 阅读10分钟

JVM系列文章:

  1. JVM(一)—— JVM内存管理
  2. JVM(二)—— 对象的创建、内存布局以及访问定位
  3. JVM(三)—— 垃圾回收机制
  4. JVM(四)—— JVM垃圾收集器

1. 常见的垃圾收集器

image.png 连线的垃圾收集器可以搭配使用。

2. Serial收集器/Serial Old收集器

Serial收集器

Serial收集器是最基础、历史最悠久的收集器。Serial收集器是一个单线程收集器,一般用在客户端模式下。他在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束(STW)。这种垃圾回收器只适合几十兆到一两百兆的堆空间进行垃圾回收(可以控制停顿时间在 100ms 以内),但是对于超过这个大小的内存回收速度很慢。

暂停工作线程,Stop the world(STW),运行一会儿就要暂停几分钟,带来了恶劣的用户体验。

下图为Serial/Serial Old收集器运行示意图:

image.png

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,它也可能有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用,另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。

3. ParNew收集器

ParNew收集器实质上是Serial收集器的多线程并行版本

下图为ParNew/Serial Old收集器运行示意图:

image.png 除了Serial收集器外,目前只有它能与CMS收集器配合工作。

4. Parallel Scavenge/Parallel Old收集器

Parallel Scavenge收集器

CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。

停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。

参数设置:

  • -XX:+UseParallelGC:新生代使用 Parallel Scavenge,老年代使用 Parallel Old。
  • -XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间。垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的,参数的值设置得小一点会导致垃圾收集发生更加频繁。
  • -XX:GCTimeRatio:设置吞吐量大小。垃圾收集时间占总时间的比率,相当于吞吐量的倒数。例如:把此参数设置为 19, 那允许的最大垃圾收集时间占用总时间的 5% (即 1/(1+19))。
  • -XX:+UseAdaptiveSizePolicy:这是一个开关参数,默认开启,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics)。

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。这个收集器是直到JDK 6时才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于相当尴尬的状态,原因是如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PSMarkSweep)收集器以外别无选择,其他表现良好的老年代收集器,如CMS无法与它配合工作。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用Parallel Scavenge收集器也未必能在整体上获得吞吐量最大化的效果。同样,由于单线程的老年代收集中无法充分利用服务器多处理器的并行处理能力,在老年代内存空间很大而且硬件规格比较高级的运行环境中,这种组合的总吞吐量甚至不一定比ParNew加CMS的组合来得优秀。直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。

下图为Parallel Scavenge/Parallel Old收集器运行示意图:

image.png

5. CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。

从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于标记-清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:

  1. 初始标记(CMS initial mark):短暂,仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快。
  2. 并发标记(CMS concurrent mark):和用户的应用程序同时进行,进行 GC Roots 追踪的过程,标记从 GCRoots 开始关联的所有对象开始遍历整个可达分析路径的对象。这个时间比较长,所以采用并发处理(垃圾回收器线程和用户线程同时工作)。
  3. 重新标记(CMS remark):短暂,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
  4. 并发清除(CMS concurrent sweep):由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

参数设置:

  • -XX:+UseConcMarkSweepGC:表示新生代使用 ParNew,老年代的用 CMS。

下图为Concurrent Mark Sweep收集器运行示意图: image.png

并发标记阶段 —— 预清理和并发可中断预清理

CMS 的目标是降低垃圾回收时的暂停时间,所以在并发阶段要尽最大的努力去处理,如果能够在并发阶段处理被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。

所以并发标记阶段后续还有两个处理:预清理并发可中断预清理

预清理

  1. 在并发阶段,在 Eden 区中分配了一个 A 对象,A 对象引用了一个老年代对象 B(这个 B 之前没有被标记),在这个阶段就会标记对象 B 为活跃对象。
  2. 在并发标记阶段,如果老年代中有对象内部引用发生变化,会把所在的 Card 标记为 Dirty(其实这里并非使用 CardTable,而是一个类似的数据结构,叫 ModUnionTalble)通过扫描这些 Table,重新标记那些在并发标记阶段引用被更新的对象。

并发可中断预清理

  1. 处理 From 和 To 区的对象,标记可达的老年代对象,类似于预处理。
  2. 预清理的第二个阶段。

这个逻辑不会一直循环下去,打断这个循环的条件有三个(满足一个即可):

  1. 可以设置最多循环的次数 CMSMaxAbortablePrecleanLoops,默认是 0,意思没有循环次数的限制。
  2. 如果执行这个逻辑的时间达到了阈值 CMSMaxAbortablePrecleanTime,默认是 5s,会退出循环。
  3. 如果新生代 Eden 区的内存使用率达到了阈值 CMSScheduleRemarkEdenPenetration,默认 50%,会退出循环。

CMS收集器缺点

CMS是一款优秀的收集器,它最主要的优点在名字上已经体现出来:并发收集、低停顿,一些官方公开文档里面也称之为“并发低停顿收集器”(Concurrent Low Pause Collector)。

CMS收集器是HotSpot虚拟机追求低停顿的第一次成功尝试,但是它还远达不到完美的程度:

  1. CPU 敏感:CMS 对处理器资源敏感,毕竟采用了并发的收集、当处理核心数不足 4 个时,CMS 对用户的影响较大。
  2. 浮动垃圾:由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为“浮动垃圾”。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。在 1.8 的版本中老年代空间使用率阈值(92%) 备注:一个复杂的公式。不用管。当然 CMS 还有参数可以控制触发回收的条件(堆空间达到多少比例触发):CMSInitiatingOccupancyFraction CMSInitiatingOccupancyFraction 的值,如果你没设置过就是虚拟机自己的默认值,默认-1,-1 就是按照 92%来算。如果手动设置-XX:CMSInitiatingOccupancyFraction=70,那么就是按照手动的设置来算。
  3. 会产生空间碎片:标记 - 清除算法会导致产生不连续的空间碎片。
    • 空间分配效率较低:如果是连续的空间 JVM 可以通过使用指针碰撞的方式来分配,而对于这种有大量碎片的空闲链表则需要逐个访问空闲列表中的项来访问,查找可以存放新建对象的地址。
    • 空间利用效率变低:新生代晋升的对象大小大于了连续空间的大小,即使整个 Old 区的容量是足够的,但由于其不连续,也无法存放新对象。就是内存碎片导致的 Promotion Failed,Young GC 以为 Old 有足够的空间,结果到分配时,晋级的大对象找不到连续的空间存放。

垃圾回收器退化

如果发生了,Promotion Failed,那么 CMS 会退化,单线程串行 GC 模式,一般会使用 Serial Old ,因为 Serial Old 是一个单线程,所以如果内存空间很大、且对象较多时,CMS 发生这样情况会很卡。

Serial 使用使用标记-整理算法,单线程全暂停的方式,对整个堆进行垃圾收集,暂停时间要长于 CMS。