此文作者吴玲,链家网二手技术中心-业务架构部的负责人,我司JAVA大牛,江湖人称玲姐(然而并不是妹子)。
此文是玲姐在帮助兄弟部门排查一个Java应用莫名僵死问题的过程总结,希望对大家有帮助。
作者|吴 玲
编辑|蔡白银
网址|tech.lianjia.com
微信公众号|链家产品技术团队
前言
前不久有兄弟部门的同事找到我,说他们有一个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
从启动参数可以看出来:
1. jdk版本是HotSpot 8u40
2. 指定了MaxMetaspaceSize
3. 使用了CMS GC算法、增量模式
4. 指定了noclassgc
分析
处理线上问题,最要紧的一定是先尽快恢复服务,减少业务损失。 从之前的信息来推测,大概率是触发了JVM的隐藏BUG, 从历史来看,hotspot bugfix的速度还是不错的,同事先升级到最新的JDK版本尝试恢复服务,咱们再接下来分析原因。
这个选项的含义是当使用CMS算法时,是否进行类卸载(ClassUnloding)。 jdk6和jdk7的默认值都是false,从Jdk8开始默认值变为了true,也就是默认进行类卸载。
我们来看看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。
后语
其实文章里挖了一些小坑没填,我实在很懒,就当作小作业留给你去解答吧 :)
[1] http://hg.openjdk.java.net/jdk8u/jdk8u40/hotspot/file/68577993c7db/src
[2] http://www.oracle.com/technetwork/java/javase/2col/8u60-bugfixes-2620228.html
[3] http://bugs.java.com/view_bug.do?bug_id=8085965
[4] http://hg.openjdk.java.net/jdk9/jdk9/hotspot/rev/8c0e5aa4995e