cover_image

谜!JVM为何僵死

吴玲 贝壳产品技术
2017年07月16日 09:01

此文作者吴玲,链家网二手技术中心-业务架构部的负责人,我司JAVA大牛,江湖人称玲姐(然而并不是妹子)。

此文是玲姐在帮助兄弟部门排查一个Java应用莫名僵死问题的过程总结,希望对大家有帮助。



作者|吴   玲

编辑|蔡白银

网址|tech.lianjia.com

微信公众号|链家产品技术团队



前言

前不久有兄弟部门的同事找到我,说他们有一个Java应用偶尔会莫名僵死、无响应、同时有个CPU核心占用100%,不稳定复现。希望我协助看看是什么原因。

图片



现场

如果仅仅只看到僵死无响应这类描述,可能马上想到GC可能有问题,又看到CPU占用100%,又可能是存在死循环,实际情况是怎么样的呢? 

咱们要用证据来说话,所谓证据其实就是故障现场,包括但不限于:GC日志线程dumpdump业务日志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启动参数如下:图片

从启动参数可以看出来: 
1. jdk
版本是HotSpot 8u40 
2.
指定了MaxMetaspaceSize 
3.
使用了CMS GC算法、增量模式 
4.
指定了noclassgc


分析

处理线上问题,最要紧的一定是先尽快恢复服务,减少业务损失。 从之前的信息来推测,大概率是触发了JVM的隐藏BUG, 从历史来看,hotspot bugfix的速度还是不错的,同事先升级到最新的JDK版本尝试恢复服务,咱们再接下来分析原因。

MaxMetaspaceSize

顾名思义,通过这个选项指定Metaspace空间的最大值。当超过这个值时,将会触发GC对该空间进行回收。

CMSClassUnloadingEnable

这个选项的含义是当使用CMS算法时,是否进行类卸载(ClassUnloding)。 jdk6jdk7的默认值都是false,从Jdk8开始默认值变为了true也就是默认进行类卸载

noclassgc

这个选项的含义是不对class进行GC,哪怕这些class已经成为垃圾。实际等价于不进行类卸载现在相当于同时打开了CMSClassUnloadingEnablenoclassgc,那到底类还会不会被卸载呢?

我们来看看Openjdk 8u40的源码[1]里对这两个选项的使用(虽然openjdkhotspot的代码有些差别,但大部分逻辑是一样的):

A. globals.hpp L1710 
CMSClassUnloadingEnabled默认设置为true

图片

B. arguments.cppL2786 
当指定了-Xnoclassgc后实际是将ClassUnloading设置为false图片

C. concurrentMarkSweepGeneration.cppL6262 
当需要卸载类时,会去更新类的层次结构(class hierarchy),将卸载的类从对应的链表里删除图片

D. concurrentMarkSweepGeneration.cpp#update_should_unload_classes 
什么时候需要卸载类呢?从这个方法实现可以看到,是否进行类卸载有两个条件, CMSClassUnloadingEnabledExplicitGCInvokesConcurrentAndUnloadsClasses有关,但跟ClassUnloading无关图片

E. kclass.cpp#clean_weak_klass_links 
ClassUnloadingfalse时,并不会去更新类的层次结构

图片


原因

所以原因基本上就呼之欲出了,CMS下,类卸载包含关键的三步:

  1.  Unload classes and purge the SystemDictionary. 

  2. Unload nmethods. 

  3. Prune dead klasses from subklass/sibling/implementor lists.

CMSClassUnloadingEnabledtrueClassUnloadingfalse时, 实际只完成了前两步,而第三步未完成。 

也就是说 CMSClassUnloadingEnabled ClassUnloading 冲突了。

同事那边升级了新版的jdk后没有再出现过问题。 我去翻了一下jdkbugfix list 发现在8u60[2]的时候已经fix掉了这个bug[3],修复的方法也很简单[4]

ClassUnloadingfalse时, CMSClassUnloadingEnabledExplicitGCInvokesConcurrentAndUnloads也设置为false.


启示

  1. 保存好故障现场后及时恢复业务、再进行排查分析

  2. 不要随便使用自己没掌握的参数选项。

  3. 及时升级你jdk的小版本,从bugfix的list也可以看出来,其实jdk的bug也不 少 :(

  4. 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 


继续滑动看下一个
贝壳产品技术
向上滑动看下一个