七大垃圾收集器分别是Serial、ParNew、Parallel Scavenge、CMS、Serial Old、Parallel Old、G1,这些收集器都已经开始商用了。
它们之间的关系如下图:
图片来源于:《深入理解Java虚拟机 第3版》
上图中的JDK9表示从JDK9开始,CMS和Serial、ParNew和Serial Old的组合官方不再支持。
这七个垃圾收集器分别作用于不同的分代,如果两个收集器之间存在连线,就说明它们可以搭配使用。唯独G1收集器是可以作用于新生代和老年代。
垃圾收集器发展到今天为止,STW(Stop The World)始终未能彻底消除,即使7个里面最先进的G1,也要有STW。
下表是收集器之间的不同点:
收集器名字 | 是否并行收集 | 作用分代 | 是否STW | 收集算法 | 优点 | 缺点 |
---|---|---|---|---|---|---|
Serial | 单线程串行 | 新生代 | 是 | 复制 | 简单,高效,消耗额外内存最小 | 无法利用多线程优势,JVM内存不能太大 |
ParNew | 多线程并行 | 新生代 | 是 | 复制 | 可以并行收集 | 在核心数少的场景下,无法发挥优势 |
Serial Old | 单线程串行 | 老年代 | 是 | 标记-整理 | 作为CMS收集器失败后的预备方案 | 无法利用多线程优势,收集过程需要STW |
Parallel Scavenge | 多线程并行 | 新生代 | 是 | 复制 | 以吞吐量为优化目标,可以自动调节JVM内存各分代大小 | -- |
Parallel Old | 多线程并行 | 老年代 | 是 | 标记-整理 | 以吞吐量为优化目标,可以自动调节JVM内存各分代大小 | -- |
CMS | 多线程并发 | 老年代 | 部分阶段 | 标记-清除 |
以获取最短回收停顿时间为目标,应用范围广
| 产生内存碎片,有可能出现回收失败 |
G1 | 多线程并发 | 新生代和老年代 | 部分阶段 | 新生代:复制 Mixed GC:标记-整理 | 大内存收集优势明显,不会产生内存碎片,可以指定期望的停顿时间 | 占用额外内存和系统负载均高于CMS,小内存收集CMS优于G1 |
吞吐量=运行用户代码的时间/(运行用户代码的时间+运行垃圾收集的时间)
前面的收集器原理相对比较简单,下面详细展开介绍CMS和G1。
CMS
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
- 并发阶段会占用了一部分系统资源,当系统资源整体紧张时,这将造成用户程序变慢,降低处理速度,而且相比较于STW的处理方式,并发阶段的处理时间会拉长,这会造成用户程序长时间响应速度慢。
- CMS采用的是标记-清除算法,经过长时间的运行后,内存可能出现大量碎片,这将导致明明内存很充足,但是找不到连续的空间分配给大对象,最后CMS只能触发Full GC,并在Full GC时,合并整理内存碎片,这个整理过程需要移动对象,因此需要STW。java还提供另一个参数-XX:CMSFullGCsBefore-Compaction(此参数从JDK 9开始废弃),这个参数的作用是要求CMS收集器在执行过若干次(数量由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(默认值为0,表示每次进入Full GC时都进行碎片整理)。
- 浮动垃圾无法及时清理,因为清理过程是并发进行的,在标记过程结束以后,可能会有新的垃圾产生,这部分垃圾就是浮动垃圾,CMS无法在当次清理过程处理掉这部分垃圾,只能等到下次清理。
- CMS在老年代使用了92%(JDK6的默认值,比例可以通过参数数-XX:CMSInitiatingOccupancyFraction调整)的内存后就会触发垃圾收集,这是因为CMS是并发执行,在收集的过程中,CMS也要使用内存。这个比例需要根据生产环境做出合理调整,既不能太高,也不能太低,如果太低,会频繁引发垃圾收集,如果太高,CMS收集期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。
G1
G1的全称是Garbage First。G1是一个里程碑式的收集器,在JDK9以上,它已经成为默认的收集器。
G1垃圾的收集方式与之前的收集器有很大的不同。G1弱化了分代的概念,它将堆内存划分成了多个大小相等的Region,每个Region根据需要可以作为新生代或老年代的空间,收集器根据Region的角色不同采用不同的收集方法。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。内存划分为Region后,新生代和老年代分别有多个Region组成,每个分代里面Region与Region之间不要求物理上连续,分代里面Region的个数G1也可以根据需要动态调整,这样年轻代和老年代的大小便可以动态变化,因此建议不要设置年轻代大小(-Xmn设置年轻代大小),由G1自己调整,网上有介绍说如果设置了-Xmn,将导致设定的收集停顿时间无效。
G1里面除了新生代和老年代之外,还有一个特殊的区域:Humongous区域。该区域是专门存储大对象的,G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象,对于大小超过了单个Region的大对象,G1将选择N个连续的Humongous Region来存放。G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。
G1相比于之前的垃圾收集器,还有一个特点是用户可以指定每次收集的停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认200毫秒)。G1在收集时,会尽力达到这个时间。那么它是如何做到的?G1将Region作为单次回收的最小单元,这意味着每次回收要么是整个Region全部回收,要么是不回收。G1会跟踪每个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间,优先处理回收价值收益最大的那些Region。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
G1的收集步骤与CMS类似,下面详细展开介绍每个过程:
初始标记
并发标记
参考资料:
《深入理解Java虚拟机 第3版》