深入理解JVM(③)经典的垃圾收集器
前言
如果说垃圾收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者。本次要介绍的是几款“经典”的垃圾收集器,之所以被称之为“经典”,是为了与几款目前仍处于实验状态,但是执行效果上哟革命性改进的高性能低延迟收集器区分开来,虽然算不上最先进的技术,但却是在实践中千锤百炼,足够成熟,可以在商用生产环境上放心使用的全部垃圾收集器。
这些“经典”收集器之间的关系图
这七种作用于不同分代的收集器,如果两个之间存在连续,说明可以搭配使用。目前这些垃圾收集器并不都是“万能”的,所以针对于各个垃圾收集器,我们的目的是根据自己的具体场景而去选择合适的收集器。
Serial收集器
Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择。这个收集器是一个单线程工作的收集器,在进行垃圾收集时,必须暂停其他所有工作现场,直到它收集结束。
Serial/Serial Old收集器的运行过程。
对于这个收集器,虽然是单线程并且需要在暂停其他所有工作线程。但这个收集器却并不是已经被抛弃或是过时的,因为它有着优于其他收集器的地方,那就是简单高效(与其他收集器的单线程相比)。对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自如可以获得最高的线程收集效率。
ParNew
ParNew收集器实质上Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致,在实现上也是共用了相当多的代码。
ParNew收集器的工作过程示意图:
ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处。但是它有一个与功能、性能无关却很重要的优势,除了Serial收集器外,目前只有它能与CMS收集器配合工作。但是随着G1收集器的出现,从JKD9开始,官方已经不再推荐ParNew和CMS这种组合了,所以ParNew也就慢慢开始退出历史舞台了。
Parallel Scavenge收集器
Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。这款收集器的关注点和其他收集器不同,其他收集器的关注点是尽可能的缩短用户线程停顿时间,而Parallel Scavenge收集器的目标则是到达一个可控制的 吞吐量。 所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。
Parallel Scavenge收集器的运行示意图
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大拦击收集停顿时间的-XX:MaxGCPauseMilis参数以及直接这是吞吐量大小的-XX:GCTimeRatio参数。
-XX:MaxGCPauseMilis参数运行的值是一个大于0的毫秒数,收集器将尽量保证内存回收花费的时间不超过用户设定值。
-XX:GCTimeRatio参数的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。
Parallel Scavenge收集器还有一个参数-XX:+UserAdaptiveSizePolicy值得我们关注。这是一个开关参数,在被激活之后就不需要人工置顶Eden去与Survivor区的比率、以及晋升老年代对象大小等细节参数了。虚拟机会根据运行情况自行调节。自适应调节策略也是Parallel Scavenge收集器区别于ParNew收集器的一个重要特性。
Serial Old收集器
Serial Old收集器是Serial 收集器的老年代版本,也是一个单线程收集器,同样适用标记-整理算法。这个收集器的主要意义也是提供客户端模式下的HotSpot虚拟机使用。
Serial Old运行示意图:
如果在服务端模式下,Serial Old收集器可能有两种用途:一种是在JDK5以及之前的版本中与Parallel Scavengen收集器搭配使用,另外一种就是作为CMS收集器发送失败时的后备预案,在并发收集发生Concurrent Mode Failure 时使用。
Parallel Old 收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,支出多线程并发收集,基于标记-整理算实现。这个收集器是在JDK6时才提供的,之前新生代选择了Parallel Scavenge收集器,老年代只能选择Serial Old(PS MarkSweep)收集器。
直到Parallel Old 收集器出现后,“吞吐量优先”收集器终于有了笔记名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。
Parallel Old收集器工作示意图:
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
CMS是基于标记-清除算法实现的,但是整个运作过程相对于前面几种收集器来说要复杂一些,整个过程分四步骤:
1、初始标记(CMS initial mark)
2、并发标记(CMS concurrent mark)
3、重新标记(CMS remark)
4、并发清除(CMS concurrent sweep)
初始标记、重新标记这两个步骤仍然需要停止用户线程。
- 初始标记仅仅是标记一些GC Roots能直接关联到的对象,速度很快;
- 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,过程较长,单不需要停顿用户线程;
- 而重新标记是为了修正缤纷标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
- 最后的并发清除阶段,是清理掉标记阶段判断的已经死亡的对象 ,由于不需要移动存活的对象,所以这个阶段也是不需要停顿用户线程的。
由于整个过程中,最耗时的并发标记和并发清除阶段,垃圾收集器的线程都可以与用户线程一起工作,所以整体来说,CMS收集器的整个回收过程是与用户线程一起并发执行的。
下面是CMS收集器的运行示意图:
CMS是一款优秀的垃圾收集器,最主要的优点就是:并发收集、低停顿。
但是它也是有三个明星的缺点的:
1、 CMS收集器对处理器资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或是处理器计算能力)而导致应用程序变慢,降低总吞吐量。
2、由于CMS无法处理“浮动垃圾”,有可能出现内存不足导致另一次完全“Stop The World”的Full GC的产生。
浮动垃圾是指:在CMS的并发标记和并发清理阶段,用户线程是孩子继续进行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这部分垃圾就称为“浮动垃圾”。
3、因为CMS是基于“标记-清除”收集算法的,所以在收集结束后会有大量的碎片空间产生。这就会导致如果创建大对象的时候找不到连续的空间而而提前触发一次Full GC的情况。
Garbage First 收集器
Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑是的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。
G1是一款主要面向服务端应的垃圾收集器。HotSpot开发团队最初赋予它的期望是替换CMS收集器。从JDK9发布之日,G1宣告取代Parallel Scavenge 加Parallel Old组合,称为服务端模式下的默认的垃圾收集器。
G1开创了基于Region的堆内存布局是它能够实现可以由用户指定用户线程停顿时间的关键。虽然G1也遵循分代收集的设计理论,但却不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region采用不同的策略去处理,这样无理是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
G1收集器的分区示意图:
如果我们不去计算用户线程运行过程中的动作,G1收集器的运作过程大致可划分为以下四个步骤:
- 初始标记: 仅仅是标记一些GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新的对象。
- 并发标记: 从GC Roots开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,与用户线程并发执行。
- 最终标记: 对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
- 筛选回收: 负责更新Region的统计数量,对各个Region进行回收价值和成本排序,根据用户期望的停顿时间制定回收计划,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧的Region的全部空间。
下面是G1收集器的运行示意图
最后结合上节说的垃圾收集算法总结一下:
垃圾收集算法 | 垃圾收集器 | ||
---|---|---|---|
标记-清除 | CMS | ||
标记-复制 | Serial | ParNew | Parallel Scavenge |
标记-整理 | Serial Old | Parallel Old | G1 |