一个运行java程序的服务器(centos7)在最近总是出现内存使用率告警,由于没找到内存泄漏原因,总是隔几天进行重启释放内存,最近决定解决掉这个问题
以下是在测试环境复现及解决问题的过程
首先free -m查看内存使用情况,可以看到主要内存被buff/cache占用了
内存使用total=used+free+buff/cache 其中buff/cache在内存不足的时候会linux尝试进行释放
top命令查看java进程内存占用(按M以内存使用排序,按P以CPU使用排序)
jmap -heap pid 查看java进程堆内存使用情况
查看实际情况是堆内存使用并没有超限,怀疑是堆外内存泄漏,但是看top命令java进程占用的实际内存270916(264m)远远小于buff/cache的2g使用量,这时毫无头绪
由于内存占用主要是buff/cache,所以尝试清除缓存
sync
echo 1 > /proc/sys/vm/drop_caches //清除pagecache
echo 2 > /proc/sys/vm/drop_caches //清除slab分配器中对象(包括目录项缓存和inode缓存)
echo 3 > /proc/sys/vm/drop_caches //清除 pagecache 和 slab 分配器中的缓存对象
复制代码
在运行echo 2之后buff/cache使用显著下降,但是真正的内存泄漏原因还没有找到
于是开始从程序源码分析开始,由于程序不是我写的没有源代码,所以拷贝一份到测试环境,使用阿里巴巴的arthas绑定进程,反编译程序流转逻辑对应代码
java -jar arthas-boot.jar
sc com.* | grep message
jad com.cupms.message.tcp.service.impl.CUServiceImpl >/tmp/CUServiceImpl.java
jad com.cupms.message.tcp.handler.ServerIoHandler >/tmp/ServerIoHandler.java
复制代码
查看ServerIoHandler.java的主要逻辑是调用CUServiceImpl的dealCU方法,于是重点分析CUServiceImpl.java
这个方法主要两个逻辑块
1,block30中查询数据库,获取到对应的字符串
2,for循环遍历字符数组调用curl请求接口发送数据
- 可以看到这个程序没有使用数据库连接池,而是每次请求一个新建一个数据库连接,首先看是不是数据库连接没有释放干净,以下是关键代码
DBHelper db = new DBHelper();
this.conn = db.getConnection();
this.stt = this.conn.createStatement();
this.set = this.stt.executeQuery(Sql);
while (this.set.next()) {
account_string = String.valueOf(account_string) + this.set.getString(1) + ",";
}
this.stt.close();
this.conn.close();
复制代码
虽然但是连接是释放掉了的,为了测试这段代码是否有问题,注释掉下面否循环块,只运行数据库查询块逻辑看是否内存泄漏,使用arthas编译加载修改过的代码
mc /tmp/CUServiceImpl.java
retransform /root/src_message_8240/bin/com/cupms/message/tcp/service/impl/CUServiceImpl.class
复制代码
写一个脚本循环调用程序接口,观察内存使用变化情况发现并没有显著增长,于是排除第一段逻辑导致泄漏的情况
-
由于是buff/cache在增长,与文件的读写有关,怀疑是不是与log4j写日志存在bug导致,注释掉打日志代码,发现内存依旧增长,排除这种可能
-
排除掉上面两种情况,分析下一段代码
BufferedReader read=null;
InputStream in=null;
InputStreamReader inReader=null;
pro=Runtime.getRuntime().exec(endCmds); //1
result = pro.waitFor(); //2
in = pro.getInputStream(); //2
inReader=new InputStreamReader(in); //2
read = new BufferedReader(inReader); //2
String line = null;
while ((line = read.readLine()) != null) {
LOGGER.info("curl命令返回值:" + line);
}
in.close(); //2
inReader.close(); //2
read.close(); //2
pro.destroy(); //1
复制代码
可以看到缓冲区释放了,但是buff/cache一直增长
逐段注释动态加载测试:
- 只运行注释1代码持续增长
- 只运行注释2代码,此时pro没有调用为空,缓存区也不会获取到,应该不会增长,实际情况与猜想一致。
于是就定位到是注释2代码这里导致的内存持续增长。
分析注释2代码,这里是使用pro=Runtime.getRuntime().exec(endCmds)调用curl请求第三方接口,桉说最后pro也被destroy掉了,内存怎么还是在增长呢?
pro.getOutputStream().close();
pro.getInputStream().close();
pro.getErrorStream().close();
pro.destroy();
pro=null;
复制代码
如上加了一些代码释放掉pro的缓冲区,测试还是在增长。于是注释掉原程序代码,重新写了一遍相同的逻辑进行测试
String[] cmd={"/bin/sh","-c","curl -H \"Content-type: application/json\" -X POST -d hhhh www.baidu.com"};
pro=Runtime.getRuntime().exec(cmd);
...
in.close(); //2
inReader.close(); //2
read.close(); //2
pro.destroy(); //1
复制代码
再次进行测试,发现内存buff/cache没有增长,也就是说pro被释放掉了
头大,为什么我测试的代码内存不会增长,但是源程序的相同逻辑代码内存就会一直增长呢?
仔细查看发现,唯一的区别就是测试的cmd字符数组和源程序的不一样,换成源程序的字符数组
String[] cmd={"/bin/sh","-c", "curl -H \"Content-type: application/json\" -X POST -d '....' https://www.baidu.com"};
复制代码
再次测试,内存增长了!
仔细一看我测试的使用的地址是www.baidu.com,而源程序的是https ://www.baidu.com, 源程序请求的是https接口。到这里可以看出是应该curl请求https惹得祸,与java程序无关了
为了印证猜想,写个shell脚本使用curl分别调用http和https接口观察内存使用情况
j=$1;
for ((i=1; i<=j; i++))
do
echo "第"$i"次循环"
k=`expr $i % 100`
if [ $k -eq 0 ]
then
free -m
cat /proc/meminfo |grep Slab
cat /proc/meminfo |grep SReclaimable
fi
if [ $2 -eq 1 ]
then
/bin/sh -c "curl -H \"Content-type: application/json\" -X POST -d hhhh https://www.baidu.com" >/dev/null
else
/bin/sh -c "curl -H \"Content-type: application/json\" -X POST -d hhhh www.baidu.com" >/dev/null
fi
done
复制代码
经过观察发现调用https接口的时候内存就会增长,搜索一下curl内存泄漏,找到了相关的文章
farll.com/2016/10/hig…
www.ddnpc.com/meem-slab.h…
blog.huoding.com/2015/06/10/…
可以看到是curl在请求https资源时会调用nss库,而低版本的nss-softokn(3.16.0及以下)存在内存泄漏bug
NSS为了检测访问的临时目录是本地的还是网络资源, 它会访问数百个不存在的文件并统计所需要的时间, 在这过程就会为这些不存在的文件生成大量dentry cache. 当curl请求产生的dentry cache超过系统的内存回收能力时, 内存使用率自然会逐步攀升. 有篇外文blog有比较详细的介绍, 以及. NSS从后面的版本开始解决了这个bug: NSS now avoids calls to sdb_measureAccess in lib/softoken/sdb.c s_open if NSS_SDB_USE_CACHE is “yes”
升级nss-softokn到nss-softokn-3.53.1-6.el7_9.x86_64,nss和curl也同步进行了升级
yum update -y nss-softokn
yum update -y nss
yum update -y curl
复制代码
再次测试内存不再持续增长,至此问题解决。
ps,测试环境我没有加NSS_SDB_USE_CACHE=yes就可以,在生产环境升级后在/etc/profile加了export NSS_SDB_USE_CACHE=yes 这个实际有没有影响没有去详细印证