[转帖]解Bug之路-记一次JVM堆外内存泄露Bug的查找
https://zhuanlan.zhihu.com/p/245401095
解Bug之路-记一次JVM堆外内存泄露Bug的查找
前言
JVM的堆外内存泄露的定位一直是个比较棘手的问题。此次的Bug查找从堆内内存的泄露反推出堆外内存,同时对物理内存的使用做了定量的分析,从而实锤了Bug的源头。笔者将此Bug分析的过程写成博客,以飨读者。
由于物理内存定量分析部分用到了linux kernel虚拟内存管理的知识,读者如果有兴趣了解请看ulk3(《深入理解linux内核第三版》)
内存泄露Bug现场
一个线上稳定运行了三年的系统,从物理机迁移到docker环境后,运行了一段时间,突然被监控系统发出了某些实例不可用的报警。所幸有负载均衡,可以自动下掉节点,如下图所示:

登录到对应机器上后,发现由于内存占用太大,触发OOM,然后被linux系统本身给kill了。
应急措施
紧急在出问题的实例上再次启动应用,启动后,内存占用正常,一切Okay。
奇怪现象
当前设置的最大堆内存是1792M,如下所示:
-Xmx1792m -Xms1792m -Xmn900m -XX:PermSi
ze=256m -XX:MaxPermSize=256m -server -Xss512k
查看操作系统层面的监控,发现内存占用情况如下图所示:

上图蓝色的线表示总的内存使用量,发现一直涨到了4G后,超出了系统限制。
很明显,有堆外内存泄露了。
查找线索
gc日志
一般出现内存泄露,笔者立马想到的就是查看当时的gc日志。
本身应用所采用框架会定时打印出对应的gc日志,遂查看,发现gc日志一切正常。对应日志如下:

查看了当天的所有gc日志,发现内存始终会回落到170M左右,并无明显的增加。要知道JVM进程本身占用的内存可是接近4G(加上其它进程,例如日志进程就已经到4G了),进一步确认是堆外内存导致。
排查代码
打开线上服务对应对应代码,查了一圈,发现没有任何地方显式利用堆外内存,其没有依赖任何额外的native方法。关于网络IO的代码也是托管给Tomcat,很明显,作为一个全世界广泛流行的Web服务器,Tomcat不大可能有堆外内存泄露。
进一步查找
由于在代码层面没有发现堆外内存的痕迹,那就继续找些其它的信息,希望能发现蛛丝马迹。
Dump出JVM的Heap堆
由于线上出问题的Server已经被kill,还好有其它几台,登上去发现它们也 占用了很大的堆外内存,只是还没有到触发OOM的临界点而已。于是就赶紧用jmap dump了两台机器中应用JVM的堆情况,这两台留做现场保留不动,然后将其它机器迅速重启,以防同时被OOM导致服务不可用。
使用如下命令dump:
jmap -dump:format=b,file=heap.bin [pid]
使用MAT分析Heap文件
挑了一个heap文件进行分析,堆的使用情况如下图所示:

一共用了200多M,和之前gc文件打印出来的170M相差不大,远远没有到4G的程度。
不得不说MAT是个非常好用的工具,它可以提示你可能内存泄露的点:

这个cachedBnsClient类有12452个实例,占用了整个堆的61.92%。
查看了另一个heap文件,发现也是同样的情况。这个地方肯定有内存泄露,但是也占用了130多M,和4G相差甚远。
查看对应的代码
系统中大部分对于CachedBnsClient的调用,都是通过注解Autowired的,这部分实例数很少。
唯一频繁产生此类实例的代码如下所示:
@Override
public void fun() {
BnsClient bnsClient = new CachedBnsClient();
// do something
return ;
}
此CachedBnsClient仅仅在方法体内使用,并没有逃逸到外面,再看此类本身
public class CachedBnsClient {
private ConcurrentHashMap<String, List<String>> authCache = new ConcurrentHashMap<String, List<String>>();
private ConcurrentHashMap<String, List<URI>> validUriCache = new ConcurrentHashMap<String, List<URI>>();
private ConcurrentHashMap<String, List<URI>> uriCache = new ConcurrentHashMap<String, List<URI>>();
......
}
没有任何static变量,同时也没有往任何全局变量注册自身。换言之,在类的成员(Member)中,是不可能出现内存泄露的。
当时只粗略的过了一过成员变量,回过头来细想,还是漏了不少地方的。
更多信息
由于代码排查下来,感觉这块不应该出现内存泄露(但是事实确是如此的打脸)。这个类也没有显式用到堆外内存,而且只占了130M,和4G比起来微不足道,还是先去追查主要矛盾再说。
使用jstack dump线程信息
现场信息越多,越能找出蛛丝马迹。先用jstack把线程信息dump下来看下。
这一看,立马发现了不同,除了正常的IO线程以及框架本身的一些守护线程外,竟然还多出来了12563多个线程。
"Thread-5" daemon prio=10 tid=0x00007fb79426e000 nid=0x7346 waiting on condition [0x00007fb7b5678000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at com.xxxxx.CachedBnsClient$1.run(CachedBnsClient.java:62)
而且这些正好是运行再CachedBnsClient的run方法上面!这些特定线程的数量正好是12452个,和cachedBnsClient数量一致!
再次check对应代码
原来刚才看CachedBnsClient代码的时候遗漏掉了一个关键的点!
public CachedBnsClient(BnsClient client) {
super();
this.backendClient = client;
new Thread() {
@Override
public void run() {
for (; ; ) {
refreshCache();
try {
Thread.sleep(60 * 1000);
} catch (InterruptedException e) {
logger.error("出错", e);
}
}
}
......
}.start();
}
这段代码是CachedBnsClient的构造函数,其在里面创建了一个无限循环的线程,每隔60s启动一次刷新一下里面的缓存!
找到关键点
在看到12452个等待在CachedBnsClient.run的业务的一瞬间笔者就意识到,肯定是这边的线程导致对外内存泄露了。下面就是根据线程大小计算其泄露内存量是不是确实能够引起OOM了。
发现内存计算对不上
由于我们这边设置的Xss是512K,即一个线程栈大小是512K,而由于线程共享其它MM单元(线程本地内存是是现在线程栈上的),所以实际线程堆外内存占用数量也是512K。进行如下计算:
12563 * 512K = 6331M = 6.3G
整个环境一共4G,加上JVM堆内存1.8G(1792M),已经明显的超过了4G。
(6.3G + 1.8G)=8.1G > 4G
如果按照此计算,应用应用早就被OOM了。
怎么回事呢?
为了解决这个问题,笔者又思考了好久。如下所示:
Java线程底层实现
JVM的线程在linux上底层是调用NPTL(Native Posix Thread Library)来创建的,一个JVM线程就对应linux的lwp(轻量级进程,也是进程,只不过共享了mm_struct,用来实现线程),一个thread.start就相当于do_fork了一把。
其中,我们在JVM启动时候设置了-Xss=512K(即线程栈大小),这512K中然后有8K是必须使用的,这8K是由进程的内核栈和thread_info公用的,放在两块连续的物理页框上。如下图所示:

众所周知,一个进程(包括lwp)包括内核栈和用户栈,内核栈+thread_info用了8K,那么用户态的栈可用内存就是:
512K-8K=504K
如下图所示:

Linux实际物理内存映射
事实上linux对物理内存的使用非常的抠门,一开始只是分配了虚拟内存的线性区,并没有分配实际的物理内存,只有推到最后使用的时候才分配具体的物理内存,即所谓的请求调页。如下图所示:

查看smaps进程内存使用信息
使用如下命令,查看
cat /proc/[pid]/smaps > smaps.txt
实际物理内存使用信息,如下所示:
7fa69a6d1000-7fa69a74f000 rwxp 00000000 00:00 0
Size: 504 kB
Rss: 92 kB
Pss: 92 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 92 kB
Referenced: 92 kB
Anonymous: 92 kB
AnonHugePages: 0 kB
Swap: 0 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
7fa69a7d3000-7fa69a851000 rwxp 00000000 00:00 0
Size: 504 kB
Rss: 152 kB
Pss: 152 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 152 kB
Referenced: 152 kB
Anonymous: 152 kB
AnonHugePages: 0 kB
Swap: 0 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
搜索下504KB,正好是12563个,对了12563个线程,其中Rss表示实际物理内存(含共享库)92KB,Pss表示实际物理内存(按比例共享库)92KB(由于没有共享库,所以Rss==Pss),以第一个7fa69a6d1000-7fa69a74f000线性区来看,其映射了92KB的空间,第二个映射了152KB的空间。如下图所示:

挑出符合条件(即size是504K)的几十组看了下,基本都在92K-152K之间,再加上内核栈8K
(92+152)/2+8K=130K,由于是估算,取整为128K,即反映此应用平均线程栈大小。
注意,实际内存有波动的原因是由于环境不同,从而走了不同的分支,导致栈上的增长不同。
重新进行内存计算
JVM一开始申请了
-Xmx1792m -Xms1792m
即1.8G的堆内内存,这里是即时分配,一开始就用物理页框填充。
12563个线程,每个线程栈平均大小128K,即:
128K * 12563=1570M=1.5G的对外内存
取个整数128K,就能反映出平均水平。再拿这个128K * 12563 =1570M = 1.5G,加上JVM的1.8G,就已经达到了3.3G,再加上kernel和日志传输进程等使用的内存数量,确实已经接近了4G,这样内存就对应上了!(注:用于定量内存计算的环境是一台内存用量将近4G,但还没OOM的机器)
为什么在物理机上没有应用Down机
笔者登录了原来物理机,应用还在跑,发现其同样有堆外内存泄露的现象,其物理内存使用已经达到了5个多G!幸好物理机内存很大,而且此应用发布还比较频繁,所以没有被OOM。
Dump了物理机上应用的线程,
一共有28737个线程,其中28626个线程等待在CachedBnsClient上。
同样用smaps查看进程实际内存信息,其平均大小依旧为
128K,因为是同一应用的原因
继续进行物理内存计算
1.8+(28737 * 128k)/1024K =(3.6+1.8)=5.4G
进一步验证了我们的推理。
这么多线程应用为什么没有卡顿
因为基本所有的线程都睡眠在
Thread.sleep(60 * 1000);//一次睡眠60s
上。所以仅仅占用了内存,实际占用的CPU时间很少。
总结
查找Bug的时候,现场信息越多越好,同时定位Bug必须要有实质性的证据。例如内存泄露就要用你推测出的模型进行定量分析。在定量和实际对不上的时候,深挖下去,你会发现不一样的风景!
[转帖]解Bug之路-记一次JVM堆外内存泄露Bug的查找的更多相关文章
- 解Bug之路-记一次JVM堆外内存泄露Bug的查找
解Bug之路-记一次JVM堆外内存泄露Bug的查找 前言 JVM的堆外内存泄露的定位一直是个比较棘手的问题.此次的Bug查找从堆内内存的泄露反推出堆外内存,同时对物理内存的使用做了定量的分析,从而实锤 ...
- 解Bug之路-记一次中间件导致的慢SQL排查过程
解Bug之路-记一次中间件导致的慢SQL排查过程 前言 最近发现线上出现一个奇葩的问题,这问题让笔者定位了好长时间,期间排查问题的过程还是挺有意思的,正好博客也好久不更新了,就以此为素材写出了本篇文章 ...
- 解Bug之路-记一次存储故障的排查过程
解Bug之路-记一次存储故障的排查过程 高可用真是一丝细节都不得马虎.平时跑的好好的系统,在相应硬件出现故障时就会引发出潜在的Bug.偏偏这些故障在应用层的表现稀奇古怪,很难让人联想到是硬件出了问题, ...
- 解Bug之路-记一次对端机器宕机后的tcp行为
解Bug之路-记一次对端机器宕机后的tcp行为 前言 机器一般过质保之后,就会因为各种各样的问题而宕机.而这一次的宕机,让笔者观察到了平常观察不到的tcp在对端宕机情况下的行为.经过详细跟踪分析原因之 ...
- 解Bug之路-记一次线上请求偶尔变慢的排查
解Bug之路-记一次线上请求偶尔变慢的排查 前言 最近解决了个比较棘手的问题,由于排查过程挺有意思,于是就以此为素材写出了本篇文章. Bug现场 这是一个偶发的性能问题.在每天几百万比交易请求中,平均 ...
- 抓到 Netty 一个隐藏很深的内存泄露 Bug | 详解 Recycler 对象池的精妙设计与实现
欢迎关注公众号:bin的技术小屋,如果大家在看文章的时候发现图片加载不了,可以到公众号查看原文 本系列Netty源码解析文章基于 4.1.56.Final版本 最近在 Review Netty 代码的 ...
- 解Bug之路-记一次调用外网服务概率性失败问题的排查
前言 和外部联调一直是令人困扰的问题,尤其是一些基础环境配置导致的问题.笔者在一次偶然情况下解决了一个调用外网服务概率性失败的问题.在此将排查过程发出来,希望读者遇到此问题的时候,能够知道如何入手. ...
- dbcp 1.4 底层连接断开时内存泄露bug
在dbcp 1.4中,如果底层的连接已经与数据库断开了,此时dbcp 1.4的实现并不释放内部连接,虽然早已提供了removeAbandoned和removeAbandonedTimeout参数,但是 ...
- Netty基础系列(4) --堆外内存与零拷贝详解
前言 到目前为止,我们知道Nio当中有三个最最核心的组件,分别是:Selelctor,Channel,Buffer.在Netty基础系列(3) --彻底理解NIO 这一篇文章中只是进行了大致的介绍. ...
- 一个神奇的bug:OOM?优雅终止线程?系统内存占用较高?
摘要:该项目是DAYU平台的数据开发(DLF),数据开发中一个重要的功能就是ETL(数据清洗).ETL由源端到目的端,中间的业务逻辑一般由用户自己编写的SQL模板实现,velocity是其中涉及的一种 ...
随机推荐
- 解决vps掉线问题
解决vps掉线问题 常见现象 在有时候遇到网络或者断电等一系列突发状况时,可能会导致在传输大文件或是好不容易拿到一个session断连了,所以有了这次学习解决这个问题的记录 场景复现 这里直接用kal ...
- java.time包中的类如何使用
java.time包是在java8中引入的日期和时间处理API,提供了一组全新的类,用于更灵活.更强大的处理日期和时间. 常用用法 1.localDate 表示日期,不包含时间和时区信息 import ...
- python 处理pdf加密文件
近期有同事需要提取加密的pdf文件,截取其中的信息,并且重构pdf文件.网上没有搜到相关的pdf操作,于是咨询了chatgpt,给出了pypdf2的使用案例.但是时间比较久远了,很多库内的调用接口都已 ...
- 你的JoinHint为什么不生效
本文分享自华为云社区<你的JoinHint为什么不生效[绽放吧!GaussDB(DWS)云原生数仓]>,作者:你是猴子请来的救兵吗 . 引言 提起数据库的Hint,几乎每个DBA都知道这一 ...
- 全量通过,华为云GaussDB首批完成信通院全密态数据库评测
摘要:100%全量通过!基于全栈创新计算架构的全密态数据库华为云GaussDB,完成了中国信通院组织的首批"全密态数据库"产品能力评测. 本文分享自华为云社区<全量通过!华为 ...
- 探究Python源码,终于弄懂了字符串驻留技术
摘要:在本文中,我们将深入研究 Python 的内部实现,并了解 Python 如何使用一种名为字符串驻留(String Interning)的技术,实现解释器的高性能. 每种编程语言为了表现出色,并 ...
- 自从安上了“AI”,这些商务经理天天按时下班了
摘要:能不能用AI来提升合同管理的效率呢?华为公司用自己的AI实践提交了一份教科书级别的答卷. 对于企业的商务精英而言,什么事情令他们既"煎熬"又"开心",既& ...
- 你眼中的程序员 VS 程序员眼中的自己,是时候打破代沟了
摘要:修电脑?格子衫?脱发?程序员被误解了怎么办?如何一句话向父母说明白你的工作? 有人说,你们程序员工作赚钱真简单,电脑上按按键盘就行了,一点也不辛苦. 有人说,程序员不懂生活,就知道天天对着电脑. ...
- 深度克隆从C#/C/Java漫谈到JavaScript真复制
如果只想看js,直接从JavaScript标题开始. 在C#里面,深度clone有System.ICloneable.创建现有实例相同的值创建类的新实例 克隆原理 值类型变量与引用类型变量 如果我们有 ...
- 火山引擎DataLeap数据质量动态探查及相关前端实现
更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 需求背景 火山引擎DataLeap数据探查上线之前,数据验证都是通过写SQL方式进行查询的,从编写SQL,到解析运 ...