说明:记录一次JVM内存溢出的排查过程;
场景
项目开发完成后,首次提交到测试环境。测试、产品同事反馈页面先是操作响应慢,抛出超时异常,最后直接无法使用。查看日志后得知是内存溢出
。
重启服务后,我对前端主要的几个长RT接口,使用Arthas进行了trace
;
排查
Step1:SQL问题
锁定到了时间主要消耗在一次数据库操作,首先想到这段SQL是否有问题。
但我把SQL单独取出来,拼接参数执行,时长非常短,不应该啊。
Step2:拦截器
这期间,我做了很多尝试,看了很多JVM内存溢出的文章,但是基本没有帮助。于是,我换了一种思路,项目重启后,疯狂调用慢接口,然后使用Arthas的dashboard
命令,查看是否有异常的进程,结果如下:
可以看到有进程被阻塞了,再使用thread id
命令,查看该进程情况,如下:
从上往下看,找到自己项目中的代码,按图索骥,我找到项目中的下面这段代码。这段代码在拦截器类里,拦截器是对操作数据库拼接参数、封装结果集时进行拦截操作,该段代码是封装结果集时会执行的。
Cipher cipher = Cipher.getInstance(algorithmName, getSingleInstance());
问题可能出现在操作数据库,拦截器操作返回结果集的这一步,我先对该方法进行追踪,如下:
很明显,问题出在这里,每条记录耗时大几百毫秒,十条记录就是大几秒;
Step3:单例对象
Cipher cipher = Cipher.getInstance(algorithmName, getSingleInstance());
回过头来,再分析这行代码。该方法是jdk提供的,应该没啥问题,重点是参数列表中的第二个方法getSingleInstance()
,该方法写在该类里,是成员方法,目的是创建单例对象作为参数。查看该成员方法,代码如下(已脱敏)
private static synchronized SingleBean getSingleInstance() {
SingleBean single = null;
if(single == null){
single = new SingleBean();
}
return single;
}
该代码看上去怪怪的,单例对象不应该这么创建。于是我将代码修改成如下,即单例创建模式之一的双重检查锁定:
private static volatile SingleBean single = null;
private ThisClass() {
}
/**
* 创建单例对象
*/
private static SingleBean getSingleInstance() {
if(single == null){
synchronized (ThisClass.class) {
if(single == null){
single = new SingleBean();
}
}
}
return single;
}
修改完之后,再调用一次接口,结果如下:
Nice!就是这个单例对象创建的问题。
复盘
这个问题,前前后后花了一整天的时间,从早上搞到晚上,最终解决,简单复盘一下,分为代码问题,其他问题;
代码
private static synchronized SingleBean getSingleInstance() {
SingleBean single = null;
if(single == null){
single = new SingleBean();
}
return single;
}
这段创建单例对象的代码,经分析可知:
-
static:导致了创建的对象不会被JVM回收;
-
synchronized:导致了线程的阻塞;
-
错误的创建方式:导致每次查询都会创建大量的对象(每查一条记录就创建一个);
综合结果,就是JVM内存溢出,系统越用越慢,最终崩溃。
其他
其他原因来自两方面,场外因素和自身原因。
-
场外因素:本身项目进度紧张,刚上测试,问题就提了一堆,而我们一个JVM溢出问题就搞了一天,产品同事也慌了,当天开了两次碰头会,又压缩了我们解决问题的时间。当时真有点“软件危机”的感觉。
-
自身原因:排查思路开始有问题,因为页面上并不是所有接口都慢,大部分是正常的。所以我分析是两个问题,一个是慢接口,一个是导致系统崩溃的JVM内存溢出的问题,应该要优先解决后者。没想到这两个是同一个问题。另外,就是被
java.lang.OutOfMemoryError
唬住了,总感觉是非常不容易发现的问题,搞得我们还去找JVM的视图化工具,看JVM相关参数,但这些除了让我们知道溢出了毫无帮助。
另外
在解决问题过程中,学会了一些JVM相关的命令和工具,在这里Mark一下;
JVM 视图工具
JDK自带视图化工具,如果你安装了JDK,可在CMD窗口敲下面的命令打开;
打开后,可查看Java进程相关的JVM参数,也支持远程连接,当然需要远程那边运行Java时配置相关参数,还需要主机可被访问;
JVM 内存泄漏排查
参考下面这篇文章,当时也敲了一些命令来排查问题。
- Java诊断工具-Arthas保姆级教程
当时,我对测试环境项目敲了下图的代码,确实可以发现FGC频繁,且时长越来越长;