一、前言
我们线上有个应用程序,大约每隔一个星期左右就会出现假死现象,也就是进程在,但是实际程序已经挂掉了,开始排查没有发现什么问题,然后就写了一个每周重启进程的脚本,定时重启进程,好了一段时间。大约在一周前,发现重启脚本也不好使了,每次重启后,大约不到十五分钟进程就会挂掉,意识到问题的严重性了,故开始以下的排查:
二、正文
1、首先通过jmap命令看下堆中各个区域信息是否正常,命令如下:
jmap -heap pid
结果如下:
我去,这年轻代、老年代和永久代都已经炸了。。。
我首先想到的是永久代空间太小了,因为是默认值,只有20M-82M,所用利用-XX:PermSize和-XX:MaxPermSize参数设置初始值和最大值。设置好了,重启进程,又跑了一段时间,出现以下结果:
额,其他都代都还可以,就是老年代又被打满了。。。
正常的话,老年代快满的时候,会进程Full GC的,但是看上面的结果Full GC好像是没有起作用。
2、使用jstat命令输出年轻代,老年代已经使用容量的百分比和gc次数,命令如下:
jstat -gcutil pid 1000 100
上面的含义就是1000毫秒统计一次gc情况,统计100次。(pid是要查看的进程)
结果如下:
这是修改完参数,大约跑了一个晚上的情况。
what???将近7000的Full GC,你咋不上天呢???程序还能跑才怪呢。。。
然后我又重启了进程,重启观察GC的变化,结果变化如下:
以上三个大约运行了半个多小时,可以看到老年代已经满了,而且Full GC已经多到不行了,程序实际已经假死了。
因为我们用的GC回收器是 Parallel Scavenge + Parallel Old,Full GC的时候会导致STW,也就是只有GC线程在工作,工作线程被挂起,所以我们的程序出现了一直假死的现象。
3、其实我们后台是之前也是有那种NullPointExcepiton错误日志的,但是排查的时候发下没有影响正常的业务,我们的业务属于和终端交互的,通过tr069协议做业务下发的,我看这个终端是正常开户的,所以就没有太在意,但正是这个NullPointExcepiton导致我们的程序假死。PS:上面说的和终端的交互可能大家没做的都不太懂,不过没关系,你们就知道是发生了大量的NullPointExcepiton异常就好了。
4、为什么大量的NullPointException会把老年代打满,造成巨量的Full GC呢?
首先我们明确一个概念,如何识别垃圾?目前虚拟机用的都是可达性分析算法,也就是以GC Roots的对象为起点出发,形成一个引用链,如果相关的对象不在以GC Roots为起点的引用链中,则会被认为是垃圾,就会被GC回收。
好了,明确了如何识别垃圾这个概念,我们来说下为啥大量NullPointException会导致老年代被打满?因为NullPointException就是一个GC Roots对象啊,在周志明的《深入理解Java虚拟机》书中,有以下一段:
所以,有大量以NullPointException为GC Roots的对象,不能够被GC回收,导致最终都进入老年代,当老年代满了,就要进行Full GC,但是由于NullPointException实在太多了,导致巨量的Full GC,所以我们的程序很快就出现假死现象了。
接下来就是排查NullPointException的问题了,实际上是程序的bug导致的,这个就不在这里详细说了。
三、总结
遇到问题,就重启服务器,这个只是治标不治本的方法啊,我们还是要找到问题本质所在。还有就是一定要注意后台错误日志中出现的问题,也许一个小问题,不会影响什么,但是当大量同样的问题出现时,就会造成很严重的问题,毕竟千里之堤毁于蚁穴。
最后引用我很佩服的一个人经常说的话:你知道的越多,你不知道的越多!