上一篇博文记录了gc的各种算法,这篇博文记录HotSpot中的几种垃圾回收器。垃圾回收器是不同虚拟机对算法思想的实现。
这张图上面三个是新生代回收器、下面三个是老年代收集器。连线表示两者可以配合使用,例如在虚拟机中可以新生代使用Serial,老年代使用CMS。下面会介绍各种回收器的特性、原理和使用场景。收集器并没有好坏之分,只有是否适合应用的场景。
一、新生代收集器
1、Serial
顾名思义,这个收集器是“连续”的(单个GC线程)。意味着新生代执行gc时所有正常的工作线程是挂起状态。Serial是最基础的收集器。随着虚拟机的发展出现了后面的收集器,但也有其优势。简单高效,对于单核环境,没有线程交互的开销,并且适合运行在Client模式下的虚拟机。java -client, java -server可以控制jvm模式。
2、ParNew
ParNew其实就是Serial的多线程版本,除了使用多线程垃圾收集器之外(多个GC线程),其余行为包括控制参数、收集算法、STW、对象分配规则、手机策略都与Serial相同。ParNew是server模式虚拟机首选的收集器,因为目前只有它能与CMS配合使用。ParNew适合在多核的服务器上使用。
3、Parallel Scavenge(PS)
字面意思是“并行清理”,如图可知PS也是新生代收集器、多线程、也使用了复制算法。PS的关注点是吞吐量和自适应。这是与前面收集器不同的。
在这里吞吐量被定义为cpu用于运行用户代码的时间与cpu总时间之比。首先我想简单说一下我对吞吐量的理解。吞吐量的定义是单位时间传输、处理的字节数或其他单位。吞吐量大不一定意味着性能好或可用率高。对于gc来说cpu的吞吐量高意味着STW的时间会更长。对于要求响应速度的系统,如果有1分钟无法提供服务,是完全不合理的。选择吞吐量小但响应速度快的,可能是更好的方案。两者的区别是,后者可能是顿卡,但可以提供服务,前者是在某段时间内无法提供服务,但其他时间正常。前面可能会得出吞吐量大性能差这个结论,其实不然,吞吐量即效率,能响应更多的请求。
所以结论是:停顿时间越短越适合与用户交互的程序,能够提升用户体验,而高吞吐量则可以高效利用cpu时间,尽快完成运算任务,主要适合后台运算而无需交互的任务。
PS提供了两个参数用于精确控制吞吐量:最大停顿时间-XX:MaxGCPauseMillis以及吞吐量大小-XX:GCTimeRatio。GC的停顿时间缩短是牺牲了吞吐量和新生代空间,收集300M和500M新生代,肯定是前者快,但触发的频率高,假设后者是10s收集一次,每次100ms,前者是5s收集一次,每次50ms。收集速度加快,但效率变低了。
PS收集器还有一个参数-XX:+UseAdaptiveSizePolicy。字面意思是“使用自适应的大小”,他的作用也是如此。当参数打开,就不需要指定新生代的小小、Eden和Survivor的比例等细节参数,虚拟机会根据当前系统运行情况收集性能监控信息,动态调整参数,根绝前面两个参数定义的最大停顿时间和吞吐率提供最合适的停顿时间或最大的吞吐量。
二、老年代收集器
1、Serial Old
单线程老年代收集器,使用标记-整理算法。可以应用在client模式下的虚拟机。如果是server模式,1.可以跟PS配合使用 2.作为CMS收集器的后备方案,在并发收集产生Concurrent Mode Failure时使用,也是两者连线的原因。
2.Parallel Old
多线程老年代收集器,同样适用标记-整理算法。在Parallel Old出现之前,新生代的PS只能与Serial Old配合使用。尴尬的是,这种组合并不能体现出PS在吞吐量上的优势。在多核环境中其综合性能不如ParNew+CMS。Parallel Old与Parallel Scavenge的组合适合应用于注重吞吐量和cpu资源敏感的场景中。这里还需要再次说明,以上不管是单线程还是多线程都会造成STW,所有工作线程都是挂起状态。
3.CMS
Concurrent Mark Sweep 是一种以获取最短回收时间为目标的收集器。Mark Sweep -> 标记-清除算法,整个过程分为4步
- 初始标记
- 并发标记
- 重新标记
- 并发清除
初始标记和重新标记任然需要STW。初始标记仅标记GC Root直接关联的对象,速度快,单线程。并发标记进行GC Roots Tracing的过程。重新标记是为了标记并发标记期间发生变化的对象,多线程。
由于时间最长的并发标记、并发清除都是与工作线程并行的,且其余两个过程STW的时间相比并发阶段要远远小于,所以总体上来说CMS的清除过程是与用户线程并发的。
制约CMS的条件也有很多:
- 因为与用户工作线程并发,毫无疑问会降低系统的并发量。CMS默认使用(cpu数量+3)/4个线程,意味着至少25%的cpu资源是用于gc的。
- 由于在清理过程中还会继续产生新的“垃圾”,这些垃圾只能留到下次清理,这些垃圾成为“浮动垃圾”。因此CMS不会在老年代快用完时触发,需要留一部分空间给“浮动垃圾”。虚拟机中可以通过-XX:CMSInitiatingOccupancyFraction控制空间大小。如果参数设置过高,预留空间少,则容易出现老年代无法满足需求的情况,此时就会出现一次Concurrent Mode Failure。还记得前面说的Serial Old可以配合CMS使用吗,此时就会启动这种后备方案。这样停顿时间就很长了。
- CMS使用标记-清除算法,无疑会增加内存碎片。我猜测不使用标记-整理算法的原因是尽量缩短STW。如果没有足够的空间分配大对象就会提前触发一次Full GC。CMS提供了-XX:+UseCMSCompactAtFullCollection的开关参数,用于没有足够空间的情况下是否做内存碎片整理的操作。默认开启。还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,用于设置执行多少次Full GC后执行一次压缩整理操作。默认是0。这点其实很尴尬,收集操作很难做到两全,就像cap理论,保证一致性就会影响可用性,保证可用性就要牺牲一致性。
说了这么多都是纸上谈兵,但在上一篇博文中也说明过了,必然会有很多理论上的知识。至于收集器是如何标记、如何清除内存、如何整理等等,也许作为一个初学者或应用系统工程师会很难接触到,作为初学者的我还没有那么深的理解。但是如果把以上只是都掌握,对于运维自己开发的系统是很有帮助的。
ps:上面又是只说了GC没说是Young GC 还是 Full GC,请按不同年代的收集器理解就好。