前不久有兄弟部门的同事找到我,说他们有一个 Java 应用偶尔会莫名僵死、无响应、同时有个 CPU 核心占用 100%,不稳定复现。希望我协助看看是什么原因。
现场
如果仅仅只看到“僵死”,“无响应”这类描述,可能马上想到 GC 可能有问题,又看到 CPU 占用 100%,又可能是存在死循环,实际情况是怎么样的呢?
咱们要用证据来说话,所谓证据其实就是故障现场,包括但不限于:GC 日志、线程 dump、堆 dump、业务日志、CPU、内存、磁盘等资源使用情况等等。
1. thread dump
就这个问题而言,因为 CPU 占用飙到 100%,所以我们先通过线程 dump 出来的堆栈信息看看线程都在干什么。 一般情况下我们常用 jstack 命令来获取线程 dump。同事先使用了如下命令,结果命令也僵死无响应:
去掉-l 后才 dump 出了咱们的第一个证据, 内容大致如下:
查看以上信息,发现了一个很奇怪的现象,除了监听端口的主线程外,所有的线程都处于 BLOCKED 状态,并且调用栈上没有任何业务代码的痕迹。但是为什么 CPU 会占用 100%呢?
一个合理的推测是存在其他正在执行本地方法的线程,然后我让同事分别用
查看到占用 CPU100%的线程调用栈如下:
这个调用栈信息,看起来像是在遍历类结构的时候出现了死循环,目测可能是触发了 JVM 的隐藏 BUG
2.GC STAT
除了查看 GC 日志外,一般我们还经常通过 jstat 来查看 gc 情况,比如:
注意看 M 的那一列,代表 Metaspace 的使用已经接近 100%,而 Metaspace 是从 java8 开始引入,替代过去的 PermGen 空间(永久带),用来存放类的元数据等信息
3.JVM 启动参数
精简后的 JVM 启动参数如下:
从启动参数可以看出来:
jdk 版本是 HotSpot 8u40
指定了 MaxMetaspaceSize
使用了 CMS GC 算法、增量模式
指定了 noclassgc
分析
处理线上问题,最要紧的一定是先尽快恢复服务,减少业务损失。 从之前的信息来推测,大概率是触发了 JVM 的隐藏 BUG, 从历史来看,hotspot bugfix 的速度还是不错的,同事先升级到最新的 JDK 版本尝试恢复服务,咱们再接下来分析原因。
MaxMetaspaceSize
顾名思义,通过这个选项指定 Metaspace 空间的最大值。当超过这个值时,将会触发 GC 对该空间进行回收。
CMSClassUnloadingEnable
这个选项的含义是当使用 CMS 算法时,是否进行类卸载(ClassUnloding)。 jdk6 和 jdk7 的默认值都是 false,从 Jdk8 开始默认值变为了 true,也就是默认进行类卸载。
noclassgc
这个选项的含义是不对 class 进行 GC,哪怕这些 class 已经成为垃圾。实际等价于不进行类卸载。现在相当于同时打开了 CMSClassUnloadingEnable 和 noclassgc,那到底类还会不会被卸载呢?
我们来看看 Openjdk 8u40 的源码[1]里对这两个选项的使用(虽然 openjdk 跟 hotspot 的代码有些差别,但大部分逻辑是一样的):
A. globals.hpp L1710
将 CMSClassUnloadingEnabled 默认设置为 true
B. arguments.cppL2786
当指定了-Xnoclassgc 后实际是将 ClassUnloading 设置为 false
C. concurrentMarkSweepGeneration.cppL6262
当需要卸载类时,会去更新类的层次结构(class hierarchy),将卸载的类从对应的链表里删除
D. concurrentMarkSweepGeneration.cpp#update_should_unload_classes
什么时候需要卸载类呢?从这个方法实现可以看到,是否进行类卸载有两个条件, 跟 CMSClassUnloadingEnabled 和 ExplicitGCInvokesConcurrentAndUnloadsClasses 有关,但跟 ClassUnloading 无关
E. kclass.cpp#clean_weak_klass_links
当 ClassUnloading 为 false 时,并不会去更新类的层次结构
原因
所以原因基本上就呼之欲出了,CMS 下,类卸载包含关键的三步:
Unload classes and purge the SystemDictionary.
Unload nmethods.
Prune dead klasses from subklass/sibling/implementor lists.
当 CMSClassUnloadingEnabled 为 true, ClassUnloading 为 false 时, 实际只完成了前两步,而第三步未完成。
也就是说 CMSClassUnloadingEnabled 跟 ClassUnloading 冲突了。
同事那边升级了新版的 jdk 后没有再出现过问题。 我去翻了一下 jdk 的 bugfix list, 发现在 8u60[2]的时候已经 fix 掉了这个 bug[3],修复的方法也很简单[4]:
当 ClassUnloading 为 false 时, 将 CMSClassUnloadingEnabled 和 ExplicitGCInvokesConcurrentAndUnloads 也设置为 false.
启示
保存好故障现场后及时恢复业务、再进行排查分析
不要随便使用自己没掌握的参数选项。
及时升级你 jdk 的小版本,从 bugfix 的 list 也可以看出来,其实 jdk 的 bug 也不 少 :(
HotSpot 的启动参数非常之多,实际使用也分散在代码里的各个角落, 几乎没有人能完全搞清楚各个参数之间是否会有冲突, 从易用性上来说,确实比不上 JRocket 和 Zing。
本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。
原文链接:
https://mp.weixin.qq.com/s/1h8og5YZstIn2Ii1_enG1A
评论