【Java】记一次Java应用内存泄漏故障
故障现象
某JAVA应用运行一段时间后STW,老年堆在达到一定的比例引起CMS GC,并不断循环,且老年堆使用大小并没有减少,初步怀疑是有内存泄漏" title="内存泄漏">内存泄漏的情况。某些对象的引用一直存在,导致这些对象无法被CMS回收。
故障分析
进入发生CMS GC的pod,运行命令jmap -histo:live 查看堆内对象instance 分布情况,如下图:
- 从jmap命令返回可以看到,JDBC的两个类占用内存非常多,这两个类一般在数据库查询完后释放,为什么会有这么多的instance存在了。这时需要dump堆内存,分析其引用关系;
- 运行命令jmap -dump:live命令;
- 运行命令kubectl cp 下载dump文件到本地;
打开JProfile工具,分析dump文件,如下图:
双击JDBC类,选择Reference–Merge dominating references,如下图:
如下图(选择"GC roots to objects"):
如图所示,两个JDBC类的dominate reference都指向“DbSequenceImpl”这个类,罪魁祸首找到了。
再看看这个类的源码:
`
public class DbSequenceImpl implements DbSequence {
private Connection conn;
public int getValue(String code) throws Exception {
if (conn == null || conn.isClosed()) {
conn = DriverManager.getConnection(host, user, password);
}
CallableStatement cs = conn.prepareCall("call wbiao_sequence(?)");
cs.setString(1, code);
cs.execute();
ResultSet rs = cs.getResultSet();
if (rs.next()) {
return rs.getInt(1);
} else {
throw new CustomException("sequence generator error");
}
}
}`
从源码可以看出,这个类有以下几个缺点:
- 数据库的链接裸奔在程序中,虽然只会创建一个实例,但是链接的有效期完全是MySQL设定的,如下图:
wait_timeout,主要含义是:设置非交互连接(比如JDBC获取的链接)的超时时间,默认是86400,就是24小时,超过这个时间,mysql服务器会主动切断那些已经连接的。
- 这样会造成一个隐患:当达到wait_timeout后,链接已经失效,从而影响业务的正常使用;
- Statement和RsultSet没有调用close方法,无法释放,该方法说明如下:
从以上分析可知,Statement和RsultSet在没有主动释放的情况下是根据链接的状态进行释放的,也就是说当创建他们的链接失效后才会释放。
综上所述,一方面业务不停的调用getValue方法,不停的创建Statement和ResultSet。另一方面Statement和RsultSet的引用(那个裸奔的链接)一直存在而无法释放,当老年堆达到某个临界点后后触发大量毫无意义的CMS GC(内存根本无法回收)。
改善方法
1,链接必须从连接池中获取,不能自己独立创建;
2,Statement和RsultSet必须用完后调用close方法,释放资源;
总结
- 历史总是循环反复,不要忘记前人总结的经验:资源一定要关闭/回收;
- 不要裸奔的数据库链接,非常危险,老老实实用链接池;
以上是 【Java】记一次Java应用内存泄漏故障 的全部内容, 来源链接: utcz.com/a/99843.html