




下载本文档
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
1、今咱们来聊聊 JVM 堆外内存泄露的 BUG 是如何查找的前言 JVM 的堆外内存泄露的定位一直是个比较棘手的问题。此次的 Bug 查找从堆内内存的泄露反推出堆外内存, 同时对物理内存的使用做了定量的分析, 从而实锤了 Bug 的 源头。笔者将此 Bug 分析的过程写成博客,以飨读者。由于 物理内存定量分析部分用到了 linux kernel 虚拟内存管理的 知识,读者如果有兴趣了解请看 ulk3( 深入理解 linux 内核 第三版 )内存泄露 Bug 现场一个线上稳定运行了三年的系 统,从物理机迁移到 docker 环境后,运行了一段时间,突然 被监控系统发出了某些实例不可用的报警。所幸有
2、负载均 衡,可以自动下掉节点,如下图所示 :登录到对应机器上后,发现由于内存占用太大,触发 OOM ,然后被 linux 系统本身 给 kill 了。应急措施紧急在出问题的实例上再次启动应用, 启动后, 内存占用正常, 一切 Okay 。奇怪现象当前设置的最 大堆内存是 1792M ,如下所示 :-Xmx1792m -Xms1792m -Xmn900m -XX:PermSi ze=256m -XX:MaxPermSize=256m -server -Xss512k 查看操作 系统层面的监控, 发现内存占用情况如下图所示 :上图蓝色的线表示总的内存使用量,发现一直涨到了 4G 后,超出了系 统限
3、制。很明显, 有堆外内存泄露了。 推荐一个交流学习群:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis ,Netty 源码分析,高并发、高性能、分布 式、微服务架构的原理, JVM 性能优化这些成为架构师必备 的知识体系。还能领取免费的学习资源,目前受益良多:查 找线索 gc 日志一般出现内存泄露, 笔者立马想到的就是查看 当时的 gc 日志。本身应用所采用框架会定时打印出对应的gc 日志,遂查看,发现 gc 日志一切正常。对应日志如下 :查看了当天的所有 gc 日志,发现内存始终会回落到 170M 左右, 并无明显的增加。要知道 JVM 进程本身占用
4、的内存可是接 近4G(加上其它进程,例如日志进程就已经到 4G 了),进 确认是堆外内存导致。排查代码打开线上服务对应对应代 码,查了一圈,发现没有任何地方显式利用堆外内存,其没 有依赖任何额外的 native 方法。关于网络 IO 的代码也是托管给Tomcat,很明显,作为一个全世界广泛流行的Web服务器, Tomcat 不大可能有堆外内存泄露。 进一步查找由于在 代码层面没有发现堆外内存的痕迹,那就继续找些其它的信 息,希望能发现蛛丝马迹。 Dump 出 JVM 的 Heap 堆由于线 上出问题的 Server 已经被 kill ,还好有其它几台,登上去发 现它们也 占用了很大的堆外内存,
5、 只是还没有到触发 OOM的临界点而已。于是就赶紧用 jmap dump 了两台机器中应用JVM 的堆情况, 这两台留做现场保留不动, 然后将其它机器 迅速重启,以防同时被 OOM 导致服务不可用。使用如下命 令 dump:jmap -dump:format=b , file=heap.bin pid 使用 MAT分析 Heap 文件挑了一个 heap 文件进行分析,堆的使用情况MAT 是如下图所示:一共用了 200多M,和之前gc文件打印出来的170M 相差不大,远远没有到 4G 的程度。不得不说个非常好用的工具,它可以提示你可能内存泄露的点: 这个61.92%。cachedBnsClien
6、t 类有 12452 个实例,占用了整个堆的 查看了另一个heap文件,发现也是同样的情况。这个地方肯定有内存泄露,但是也占用了 130 多 M ,和 4G 相差甚远。查看对应的代码系统中大部分对于CachedBnsClient 的调用,都是通过注解 Autowired 的,这部分实例数很少。唯一频繁 产生此类实例的代码如下所示 :Override public void fun() BnsClient bnsClient = new CachedBnsClient(); / do something return ;此CachedBnsClient仅仅在方法体内使用,并没有逃逸到外面,再看此
7、类本身 public class CachedBnsClient privateConcurrentHashMap> authCache = newConcurrentHashMap>(); private ConcurrentHashMap> validUriCache = new ConcurrentHashMap>(); privateConcurrentHashMap> uriCache = new ConcurrentHashMap>();没有任何static变量,同时也没有往任何全局变量注册自身。换言之, 在类的成员 (Member) 中,是不可
8、能出现内存泄露的。当时只粗略的过了一过成员变量,回过头来细想,还是漏了 不少地方的。更多信息由于代码排查下来,感觉这块不应该 出现内存泄露 (但是事实确是如此的打脸 )。这个类也没有显式用到堆外内存, 而且只占了 130M ,和 4G 比起来微不足道, 还是先去追查主要矛盾再说。 使用 jstack dump 线程信息现场信息越多, 越能找出蛛丝马迹。 先用 jstack 把线程信息 dump来看下。这一看,立马发现了不同,除了正常的 IO 线程以及框架本身的一些守护线程外,竟然还多出来了12563 多个线程。 'Thread-5' daemon prio=10 tid=0x0
9、0007fb79426e000 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 数量致 !
10、 再次 check 对应代码原来刚才看 CachedBnsClient 代码的时候遗漏掉了一个关键的点 !publicCachedBnsClient(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); 这段代码是 Cac
11、hedBnsClient 的构造函数,其在里面创建了个无限循环的线程,每隔 60s 启动一次刷新一下里面的缓 存! 找到关键点在看到 12452 个等待在 CachedBnsClient.run的业务的一瞬间笔者就意识到,肯定是这边的线程导致对外 内存泄露了。下面就是根据线程大小计算其泄露内存量是不 是确实能够引起 OOM 了。发现内存计算对不上由于我们这 边设置的 Xss 是 512K ,即一个线程栈大小是 512K ,而由于 线程共享其它 MM 单元 (线程本地内存是是现在线程栈上的),所以实际线程堆外内存占用数量也是512K。进行如下计算:12563 * 512K = 6331M = 6
12、.3G 整个环境一共 4G,加上JVM堆内存1.8G(1792M),已经明显的超过了 4G。(6.3G 1.8G)=8.1G > 4G 如果按照此计算, 应用应用早就被 OOM 了。怎么回事呢?为了解决这个问题,笔者又思考了好久。如下 所示 :Java 线程底层实现 JVM 的线程在 linux 上底层是调用个 JVM 线程NPTL(Native Posix Thread Library) 来创建的, 就对应 linux 的 lwp( 轻量级进程,也是进程,只不过共享了mm_struct ,用来实现线程 ),一个 thread.start 就相当于 do_fork了一把。其中,我们在 J
13、VM 启动时候设置了 -Xss=512K( 即线 程栈大小 ),这 512K 中然后有 8K 是必须使用的,这 8K 是 由进程的内核栈和 thread_info 公用的, 放在两块连续的物理 页框上。如下图所示 :众所周知,一个进程 (包括 lwp) 包括内 核栈和用户栈,内核栈 thread_info 用了 8K ,那么用户态的 栈可用内存就是 :512K-8K=504K 如下图所示 :Linux 实际物理 内存映射事实上 linux 对物理内存的使用非常的抠门,一开 始只是分配了虚拟内存的线性区,并没有分配实际的物理内 存,只有推到最后使用的时候才分配具体的物理内存,即所 谓的请求调页。
14、 如下图所示 :查看 smaps 进程内存使用信息使 用如下命令,查看 cat /proc/pid/smaps > smaps.txt 实际物理 内存使用信息,如下所示 :7fa69a6d1000-7fa69a74f000 rwxp 00000000 00:00 0 Size: 504 kBRss: 92 kBPss: 92 kBShared_Clean: 0 kBShared_Dirty: 0 kBPrivate_Clean: 0 kBPrivate_Dirty: 92 kBReferenced: 92 kBAnonymous: 92 kBSwap: 0 kBKernelPageSiz
15、e: 4 kBMMUPageSize: 4 kB7fa69a7d3000-7fa69a851000 rwxp 00000000 00:00 0 Size: 504 kBRss: 152 kBPss: 152 kBShared_Clean: 0 kBShared_Dirty: 0 kBPrivate_Clean: 0 kBPrivate_Dirty: 152 kBReferenced: 152 kBAnonymous: 152 kBAnonHugePages: 0 kBSwap: 0 kBKernelPageSize: 4 kBMMUPageSize: 4 kB 搜索下 504KB ,正好是 1
16、2563 个,对了12563 个线程, 其中Rss表示实际物理内存(含共享库)92KB,Pss表示实际物理内存(按比例共享库)92KB(由于没有共享库,所以 Rss=Pss),以第一个 7fa69a6d1000-7fa69a74f000152KB线性区来看,其映射了 92KB 的空间,第二个映射了 的空间。如下图所示:挑出符合条件(即size是504K)的几组看了下, 基本都在 92K-152K 之间,再加上内核栈 8K(92152)/2 8K=130K ,由于是估算, 取整为 128K ,即反映此应用 平均线程栈大小。注意,实际内存有波动的原因是由于环境 不同,从而走了不同的分支,导致栈上的
17、增长不同。重新进 行内存计算 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的机器)为什么
18、在物理机上没有应用 Down 机笔者登录了原来物理机,应用还在跑,发现其同样有堆外内存泄露的现象,其物理内存使用已经达到了5个多G!幸好物理机内存很大,而且此应用发布还比较频繁,所以没有被OOM。 Dump 了物理机上应用的线程, 一共有 28737 个线程, 其中 28626 个线程等待在 CachedBnsClient 上。同样用 smaps查看进程实际内存信息,其平均大小依旧为128K,因为是同一应用的原因继续进行物理内存计算 1.8 (28737 * 128k)/1024K =(3.6 1.8)=5.4G 进一步验证了我们的推理。 这么多线程应用为什么没有卡顿因为基本所有的线程都睡眠在Thread.sle
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
评论
0/150
提交评论