起因

  • 来新公司后才开始使用 java 写代码,因此对于 java 这门新语言其实是边模仿变写的,其实学任何新语言最好的办法就是有一份比较高质量的代码可以参考。即便是再怎么参考别人的 java 代码,但是写服务的过程中避免不了会遇到 java 的 gc 问题,真正关注 java 的 gc 是从有一天晚上突然收到公司的告警,cms gc 的时间超过 500ms,然后上游大面积的报超时开始的。gc 其实也不是什么稀奇的玩意,目前大部分语言都会将 gc 设计进来,这对于写这门语言的人来说不需要太多关注内存的申请和释放过程。但是一般来说发生 gc 后很多人并不关心它,因为 gc 是一种正常的现象,而我却要关心它的原因在于我们的 java 程序是给别人提供 soa 服务的,而对方的请求是有超时时间的,如果 gc 的时间偏长,那么对方就会在这段时间内大量的报超时,影响用户体验。由于公司是用 java 的 cms gc 作为真正的 gc 算法的。

原理

  • 既然已经知道问题的起因,那就要先了解问题产生的原理,需要对 cms gc 的整个过程有一定的了解,否则就无从调优。一般 java 的 GC 分新生代和老年代 GC 两种,一般新生代的 GC 比较频繁,并且新生代的 GC 时间一般较短,一般在几十毫秒内,也就是说即使发生 gc,也不会对服务造成太大的影响。我们关注的主要是老年代的 GC,因为老年代的 GC 一旦发生并且耗时较长的话,对服务的影响是比较明显的,因此我们需要分析下老年代 GC 的过程中系统都做了什么事情。老年代 GC 过程中有两个阶段是 STW 的,一个是 Init mark,另一个是 remark,这两个步骤都是需要遍历整个内存堆栈的。Init mark 的过程是一个初步的标记,标记那些从 Root 往下的引用对象,remark 是一个复制清除的过程,执行真正的 gc 算法(具体的 gc 算法可以参考其他资料)

分析

  • 接下来我们就可以拿着具体的 GC 日志来分析耗时情况了。从下面的日志来看,最主要的耗时在于 Final Reference 的回收耗时,于是我开始了网上查其他人的调优经验的过程。
    • 2020-06-09T16:13:59.334+0800: 229185.862: [GC (CMS Final Remark) [YG occupancy: 620432 K (3145472 K)]
    • 2020-06-09T16:13:59.334+0800: 229185.862: [Rescan (parallel) , 0.1541535 secs]
    • 2020-06-09T16:13:59.489+0800: 229186.016: [weak refs processing
    • 2020-06-09T16:13:59.489+0800: 229186.016: [SoftReference, 645082 refs, 0.1005071 secs]
    • 2020-06-09T16:13:59.589+0800: 229186.117: [WeakReference, 0 refs, 0.0000209 secs]
    • 2020-06-09T16:13:59.589+0800: 229186.117: [FinalReference, 645398 refs, 0.4241113 secs]
    • 2020-06-09T16:14:00.013+0800: 229186.541: [PhantomReference, 0 refs, 4 refs, 0.0000205 secs]
    • 2020-06-09T16:14:00.013+0800: 229186.541: [JNI Weak Reference, 0.0000524 secs], 0.5248181 secs]
    • 2020-06-09T16:14:00.013+0800: 229186.541: [class unloading, 0.0392968 secs]
    • 2020-06-09T16:14:00.053+0800: 229186.580: [scrub symbol table, 0.0079694 secs]
    • 2020-06-09T16:14:00.061+0800: 229186.588: [scrub string table, 0.0507013 secs]
    • [1 CMS-remark: 4893898K(6990848K)] 5514330K(10136320K), 0.8375521 secs] [Times: user=1.29 sys=0.00, real=0.83 secs]

解决

  • 经过网上的多个 GC 调优的文章的阅读以及对 GC 的深入了解后,首先推荐要加的一个 GC 参数是 -XX:+CMSScavengeBeforeRemark,这个参数的意义在于在每次发生 CMS GC 的之前先强制进行一次新生代的 GC。这样有什么好处呢?因为 CMS 在 init 阶段会扫描整个堆栈,如果先进行一次新生代 GC,那么 CMS 要扫描的量就会减少,这样会减少 CMS 第二阶段扫描的量,减少第二阶段的耗时。
  • 另一个要增加的参数是 -XX:+ParallelRefProcEnabled,这个参数就是为了提高 Reference 的回收效率设置的,意思就是并行的对 Reference 进行回收。虽然 CMS 号称是并行处理的,但是估计这个步骤的处理默认是单线程的。
  • 另一个网友还推荐了减少第一个阶段的扫描数据量的方式,但是暂时没有尝试,因为它遇到的问题与方法一的方式的原理类似,就是权衡新生代扫描的数据量,减少进入第二阶段的要处理的量来减少第二阶段的耗时。