CMS 阶段性了结

CMS 阶段性了结

接触 CMS 已经很长时间了,但是对 CMS 的了解似乎一直没有开始,直到最近模型平台项目用到时遇到了很多问题。网上查阅了很多资料,主要参考了 R大、寒泉子、占小狼的各种文章以及回帖。不过自己还是有些愚钝,对一些问题至今不是很清楚,主要原因在于一方面 JVM GC 知识点很多,划分并不清晰,而到如今资料层出不穷,很多信息甚至是错误的,对知识理解与掌握造成了不小的干扰。可是有些工作要继续下去,对于不是很确定的知识点也不能一直深入钻研下去。所以在此做一个阶段性总结,把自己确定的、不确定的地方梳理一下。如果后续有时间像那些 JVM 大神一样钻研 JVM 源码,再把坑填上。其实,到如今,无论谁怎么解释我都没法相信了,因为代码 -> 作者理解 -> 语言总结 -> 个人理解,经历了多层的知识转换,恐怕到我这一步知识已经变了原有的味道,所以最好还是要自己去看源码来理解,中间两步仅仅是为了帮助理解而存在的。


所以这里想谈一谈学习的路径

  • 专家、原作者表述 -> 代码 -> 个人理解
  • 学习者阅读代码 -> 学习者理解 -> 学习者语言总结 -> 个人理解 -> 代码 -> 个人理解

其实可以看到无论如何,最终的个人理解都是要以代码为基础的,而第一种路径是能够避免少走弯路,第二种稍有不慎,就会掉入其他学习者的坑,就像现在这样。但是由于知识体量庞大,而且很多时候工作中即便没有达到最终的个人理解,也可以使用,因此第二条路径的前半部分是最常采用的学习路径。

回到 CMS,CMS 是一种并发标记清理的垃圾回收器,它不负责整理,会在老年代产生内存碎片,但是由于它是并发的(注意,和 Par 那些并行收集器不同,它的并发是指可以伴随用户线程共同执行,而且只是在一些阶段上可以做到,它的做法其实就是把垃圾回收的步骤细化,一些可以不 STW 的步骤就并发执行,而 STW 的阶段可以串行也可以并行),在除新生代以外的回收上表现要比纯 STW 表现的好,所以在前几年是备受青睐的,而且现在很多系统依然使用 ParNew + CMS 这种组合。但是 CMS 就像是提供了一种好的思路,但是实现起来却发现有很多坑,也因此留了很多灵活的接口,打了很多补丁,可以看到 CMS 相关的参数配置有一大堆,想要配合好并不容易。


为了便于理解,这里采用一种场景分析的方式来总结 CMS 的各种特性。

Young

首先看 Young 区,young 区的触发条件很简单,当 Eden 区无法再分配对象时,就会触发 Young GC。Young GC 的基本步骤包括,标记 + 清除 + 整理(整理到 Survivor 区)。一般情况下,Young 区的对象,大部分都是可以被回收掉的,剩余的对象会被放到 Survivor 区,这时候涉及到对象的复制,这个过程相比于标记更加耗时。然而并非任何时候对象都会被放入 Survivor,当某个对象过大,超过设置的阈值,就会进入老年代,如果某个 survivor 区的对象已经存活了很多代,jvm 会让它晋升到老年代,或者在 Young 回收时,如果 survivor 区无法容纳剩余的对象,一些对象也会被放入老年代。young 区回收速度很快,GC roots + 老年代对象来做可达路径分析。

  • 疑问1:如果回收时发现 survivor 区无法容纳剩余的年轻代对象,是 survior 区那些代数高的进入老年代还是无法被分配的进入老年代呢?个人猜想,是代数高的进入老年代,一方面这种方式显然更合理,另一方面,也不难实现,就是清除后,先把 eden 区存活的对象放入 To 区,然后把 From 区存活的对象按代数的低到高放入 To,如果放不下,那么就晋升。

Old

CMS 回收实际上不等同于 Full GC。因为 CMS GC 本身是由自己的一个线程来触发的,检查条件是 Old 区剩余的对象是否大于了某个比例,这个比例可以通过CMSInitiatingOccupancyFraction设置,默认是 94,设置小了就会频繁的 CMS GC,而且如果回收不掉会很麻烦,可能导致一直 CMS GC,而设置大了,由于一直不 CMS GC,导致 Full GC 的触发门槛变低。只要是使用了 CMS GC,那么这个回收机制就会存在的。它有一个别名,叫 CMS background mode,也就是后台模式,注意,这种 GC 是不会去关新生代和堆内存的。因此如果出现了跨代引用或者循环引用,CMS GC 就没有能力去解决问题。为了提高这种 CMS GC 的效果,引入了一种机制,就是在 CMS GC 之前触发一次 Young GC,这样能解决跨代引用的问题,参数为CMSScavengeBeforeRemark,字面意思上可以看出,是在 remark 的时候先做一次清扫,既 young gc,一方面能够解决跨代引用的问题,提升回收效果,另一方面,也能减少 Old 区标记的成本,因为一些引用了老年代的年轻代会作为 GC Roots。

接下来谈谈 Full GC,一般的 Full GC 条件是:

  1. 如果 JVM 往次统计的晋升对象的平均大小或者 young 区对象的大小超过了老年代剩余的空间,就会触发 Full GC
  2. System.gc() 或者外部 jmap -dump:live,jmap -histo:
  3. 如果有一个大对象,Old 区装不下
  4. permgen 或者 metaspace 空间不足
  5. young promotion failed,也就是说 survivor 空间不够了,而剩余的对象也不能全放到 Old 的时候

但是对于 CMS 来说,会相对复杂一些。因为 CMS 希望尽量减少 Full GC。那么它做了一些工作,这里先抛开主动触发 Full GC 这种方式,其他的几种方式自然是会触发 Full GC 的,CMS 默认的 Full GC 方式是 MSC,也就是标记、清理、压缩,它负责了新生代、老年代、堆外内存、永久代(一些参数可配,例如 permgen 以及 class 卸载)。而实际上,并非 Full GC 就会做压缩,因为做压缩(也就是整理)这个过程是耗时的,因此,它可以设置CMSFullGCsBeforeCompaction参数来控制 Full GC 多少次才进行压缩。有些资料说是多少次 CMS GC 才被压缩,这里我还是不敢确定,从字面上来看,应该是 Full GC,也可以理解。

接下来谈主动触发 GC,这里 System.gc() 和 jmap 命令触发不同,有些 jmap 命令是会触发 Full GC 的,有些是不会的。System.gc() 默认是触发 Full GC 的,一些 jar 包用了 System.gc(),导致自己的服务变得不可控,因此很多时候会禁掉这种方式触发 GC,而这样又会导致一个问题,就是这些 jar 包如果用了堆外内存,导致这些堆外内存无法回收,有可能会因为空间不足触发 Full GC,因此还是希望他们生效。这里就引入了另一种方式,那就是ExplicitGCInvokesConcurrent,期望 GC 调用时是并发的,这个参数我理解是专门为 System.gc 准备的,也就是这时候不会调用 MSC,而是 CMS foreground mode,对堆外内存做了回收。但是它仍然无法解决循环引用的问题,实测。所以可以理解为这种模式是 CMS 和 Full GC 的中间体,它不是 CMS Old GC,同时也不是 MSC Full GC。

  • 疑问1:System.gc 触发的这种 foreground mode 在压缩方面会被认为是 Full GC 吗,也就是会触发压缩吗?个人理解不会。
  • 疑问2:ExplicitGCInvokesConcurrent 这种设置会导致 vm 线程触发的 Full GC 不是 MSC 吗?个人理解不会,感觉这种模式只是作用于 System.gc 上,并非作用于整体的 Full GC 上。

至此,CMS 总结暂告一段落,希望以后通过读源码了解。而后应该把重点放在 G1 上。

猜你喜欢

转载自www.cnblogs.com/43726581Gavin/p/9656490.html
CMS