【Java】记一次Java应用内存泄漏故障

故障现象

某JAVA应用运行一段时间后STW,老年堆在达到一定的比例引起CMS GC,并不断循环,且老年堆使用大小并没有减少,初步怀疑是有内存泄漏" title="内存泄漏">内存泄漏的情况。某些对象的引用一直存在,导致这些对象无法被CMS回收。

故障分析

进入发生CMS GC的pod,运行命令jmap -histo:live 查看堆内对象instance 分布情况,如下图:
【Java】记一次Java应用内存泄漏故障

  • 从jmap命令返回可以看到,JDBC的两个类占用内存非常多,这两个类一般在数据库查询完后释放,为什么会有这么多的instance存在了。这时需要dump堆内存,分析其引用关系;
  • 运行命令jmap -dump:live命令;
  • 运行命令kubectl cp 下载dump文件到本地;

打开JProfile工具,分析dump文件,如下图:
【Java】记一次Java应用内存泄漏故障

双击JDBC类,选择Reference–Merge dominating references,如下图:
【Java】记一次Java应用内存泄漏故障

如下图(选择"GC roots to objects"):
【Java】记一次Java应用内存泄漏故障

如图所示,两个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设定的,如下图:

【Java】记一次Java应用内存泄漏故障
wait_timeout,主要含义是:设置非交互连接(比如JDBC获取的链接)的超时时间,默认是86400,就是24小时,超过这个时间,mysql服务器会主动切断那些已经连接的。

  • 这样会造成一个隐患:当达到wait_timeout后,链接已经失效,从而影响业务的正常使用;
  • Statement和RsultSet没有调用close方法,无法释放,该方法说明如下:

【Java】记一次Java应用内存泄漏故障
从以上分析可知,Statement和RsultSet在没有主动释放的情况下是根据链接的状态进行释放的,也就是说当创建他们的链接失效后才会释放。

综上所述,一方面业务不停的调用getValue方法,不停的创建Statement和ResultSet。另一方面Statement和RsultSet的引用(那个裸奔的链接)一直存在而无法释放,当老年堆达到某个临界点后后触发大量毫无意义的CMS GC(内存根本无法回收)。

改善方法

1,链接必须从连接池中获取,不能自己独立创建;
2,Statement和RsultSet必须用完后调用close方法,释放资源;

总结

  • 历史总是循环反复,不要忘记前人总结的经验:资源一定要关闭/回收;
  • 不要裸奔的数据库链接,非常危险,老老实实用链接池;

以上是 【Java】记一次Java应用内存泄漏故障 的全部内容, 来源链接: utcz.com/a/99843.html

回到顶部