【JVM 垃圾收集器】— 垃圾收集器
收集算法是内存回收的理论基础,垃圾收集器是内存回收的具体实现。在 HotSpot 虚拟机实现中,目前有 7 种垃圾收集器实现,分别是 Serial、ParNew、Parallel Scavenge、CMS、Serial Old 和 G1。前三种是新生代垃圾收集器,后面四种是老年代垃圾收集器。它们可以搭配使用,如下图所示(有连线才能搭配使用)
新生代
新生代垃圾收集器有 Serial、ParNew 和 Parallel Scavenge。
Serial
Serial 收集器是一个单线程的收集器,在进行垃圾收集时,必须暂停其他所有的工作线程,也就是 STW。到目前为止,Serial 垃圾收集器是虚拟机运行在 Client 模式下的默认新生代收集器。它有着优于其他收集器的地方
简单而高效,对于限定单个 CPU 的环境来说,Serial 收集器没有线程切换的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
ParNew
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了多线程收集之外,其他与 Serial 收集器相比并没有太多创新之处,但它是许多运行在 Server 模式下的虚拟机中首选的新生代收集器。其中一个与性能无关但很重要的原因是,除了 Serial 收集器,只有 ParNew 收集器能与 CMS 收集器配个工作(从上图中可以看到能与 CMS 搭配使用的就只有 Serial 和 parNew)。ParNew 收集器是使用 -XX:+UseConcMarkSweepGC 选项后的默认新生代收集器,也可以使用 -XX:+UseParNewGC 选项来强制指定它。
Parallel Scavenge
Parallel Scavenge 收集器与其他收集器的关注点不同,CMS 等收集器的关注点是尽可能的缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput)。
所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
由于与吞吐量关系密切,Parallel Scavenge 收集器也经常被称为“吞吐量优先”收集器。其中有一个参数 -XX:UseAdaptiveSizePolicy,这是一个开关参数。当打开这个参数后,就不需要手动指定新生代大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调节这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式成为 GC 自适应的调节策略。自适应调节策略是 Parallel Scavenge 收集器与 ParNew 收集器的在一个重要区别。
老年代
老年代收集器有 Serial Old、CMS、Parallel Old 和 G1。
Serial Old
Serial Old 是 Serial 收集器的老年代版本,同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是给 Client 模式下的虚拟机使用。在 Server 模式下,它的主要主要作用是作为 CMS 收集器的后备园,在并发收集反生 Concurrent Mode Failure 时使用。
Parallel Old
Parallel Old 的出现,使得“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
CMS
CMS(Concurrent Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器,适用于 B/S 系统的服务端上。CMS 收集器是基于“标记-清除”算法实现的,整个运作过程分为 4 个步骤,包括:
- 初始标记(CMS initial mark,需要 STW)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark,需要 STW)
- 并发清除(CMS concurrent sweep)
初始标记、重新标记这两个步骤仍需要 STW。初始标记仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快。并发标记阶段就是进行 GC Roots Tracing 的过程。而重新标记阶段则是为了修正并发标记期间因为用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一点,但远比并发标记的时间短。
CMS 收集器的优点是:并发收集、低停顿。但它有以下 3 个明显的缺点:
- CMS 收集器对 CPU 资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占了一部分线程(或者说 CPU 资源)而导致应用程序变慢,总吞吐量降低。
- CMS 收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次 Full GC 的产生。在并发清除阶段,用户线程还在运行,那就会产生新的垃圾。这一部分垃圾 CMS 在这一次回收中不能回收,只能等到下一次 GC 时再清理掉。也就是因为用户线程还在运行,那就需要预留足够的空间给用户线程使用,所以 CMS 收集器不能像别的收集器那样等到老年代几乎被填满再进行回收。如果 CMS 预留的空间无法满足用户线程使用,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备方案:临时启动 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。参数 -XX:CMSInitiatingOccupancyFraction 用来设置触发 CMS 收集百分比,设置太高容易导致“Concurrent Mode Failure”失败,性能反而降低。
- CMS 是一款基于“标记-清除”算法实现的收集器,这就意味回收后会有大量的空间碎片产生。空间碎片过多时,将会给大对象分配带来困难,往往会出现老年代还有很大空间,但是无法找到足够大的连续内存来分配当前对象,不得不提前触发一次 Full GC。为了解决这个问题,CMS 收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数(默认开启),用于在 CMS 收集器顶不住要进行 FullGC 时开启内存碎片的合并整理过程,内存整理过程是无法并发的,空间碎片问题没有了,但停顿时间变长了。JVM 还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置执行多少次不压缩的 Full GC 后,跟这来一次带压缩的(默认为 0,表示每次进入 Full GC 都进行碎片整理)。
G1
与其他 GC 收集器相比,G1具备以下特点
- 并行与并发:G1能充分用多 CPU、多核环境下硬件优势,使用多个 CPU来缩短 Stop-the-world 停顿的时间,部分其他收集器原本需要停顿Java线程执行的 GC 的动作,G1收集器仍然何以通过并发的方式让Java程序继续执行
- 分代收集
- 空间整合:与 CMS 的“标记-清理”算法不同,G1从整体上来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的
- 可预测的停顿
内存分配回收策略
- 大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起异常 Minor GC。
- 大对象(需要大量连续内存空间的对象)直接分配在老年代,虚拟机提供 -XX:PretenureSizeThreshold 参数,令大于这个设置的对象直接在老年代分配。
- 长期存活的对象将进入老年代。虚拟机给每个对象定义了一个对象年龄计数器,对象在 Survivor 区中每经历过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认 15),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 设置。
- 动态对象年龄判断。虚拟机并不是永远要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升到老年代,如果在 Survivor 区中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
- 空间分配担保:Survivor 空间无法容纳的对象直接进入老年代。