记一次JVM堆外内存泄露Bug的查找

前言

JVM的堆外内存泄露的定位一直是个比较棘手的问题。这次的Bug查找从堆内内存的泄露反推出堆外内存,同时对物理内存的使用作了定量的分析,从而实锤了Bug的源头。笔者将此Bug分析的过程写成博客,以飨读者。linux

因为物理内存定量分析部分用到了linux kernel虚拟内存管理的知识,读者若是有兴趣了解请看ulk3(《深刻理解linux内核第三版》)docker

内存泄露Bug现场

一个线上稳定运行了三年的系统,从物理机迁移到docker环境后,运行了一段时间,忽然被监控系统发出了某些实例不可用的报警。所幸有负载均衡,能够自动下掉节点,以下图所示: 缓存

clipboard.png

登陆到对应机器上后,发现因为内存占用太大,触发OOM,而后被linux系统自己给kill了。性能优化

应急措施服务器

紧急在出问题的实例上再次启动应用,启动后,内存占用正常,一切Okay。网络

奇怪现象多线程

当前设置的最大堆内存是1792M,以下所示:架构

-Xmx1792m -Xms1792m -Xmn900m -XX:PermSi
ze=256m -XX:MaxPermSize=256m -server -Xss512k并发

查看操做系统层面的监控,发现内存占用状况以下图所示: 负载均衡

clipboard.png

上图蓝色的线表示总的内存使用量,发现一直涨到了4G后,超出了系统限制。

很明显,有堆外内存泄露了。

查找线索

gc日志

通常出现内存泄露,笔者立马想到的就是查看当时的gc日志。

自己应用所采用框架会定时打印出对应的gc日志,遂查看,发现gc日志一切正常。对应日志以下:

clipboard.png

查看了当天的全部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文件进行分析,堆的使用状况以下图所示:

clipboard.png

一共用了200多M,和以前gc文件打印出来的170M相差不大,远远没有到4G的程度。

不得不说MAT是个很是好用的工具,它能够提示你可能内存泄露的点:

clipboard.png

这个cachedBnsClient类有12452个实例,占用了整个堆的61.92%。

查看了另外一个heap文件,发现也是一样的状况。这个地方确定有内存泄露,可是也占用了130多M,和4G相差甚远。

查看对应的代码

系统中大部分对于CachedBnsClient的调用,都是经过注解Autowired的,这部分实例数不多。

惟一频繁产生此类实例的代码以下所示:

clipboard.png

此CachedBnsClient仅仅在方法体内使用,并无逃逸到外面,再看此类自己

clipboard.png

没有任何static变量,同时也没有往任何全局变量注册自身。换言之,在类的成员(Member)中,是不可能出现内存泄露的。

当时只粗略的过了一过成员变量,回过头来细想,仍是漏了很多地方的。

更多信息

因为代码排查下来,感受这块不该该出现内存泄露(可是事实确是如此的打脸)。这个类也没有显式用到堆外内存,并且只占了130M,和4G比起来微不足道,仍是先去追查主要矛盾再说。

使用jstack dump线程信息

现场信息越多,越能找出蛛丝马迹。先用jstack把线程信息dump下来看下。 这一看,立马发现了不一样,除了正常的IO线程以及框架自己的一些守护线程外,居然还多出来了12563多个线程。

clipboard.png

并且这些正好是运行再CachedBnsClient的run方法上面!这些特定线程的数量正好是12452个,和cachedBnsClient数量一致!

再次check对应代码

原来刚才看CachedBnsClient代码的时候遗漏掉了一个关键的点!

clipboard.png

这段代码是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公用的,放在两块连续的物理页框上。以下图所示:

clipboard.png

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

512K-8K=504K

以下图所示:

clipboard.png

Linux实际物理内存映射

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

clipboard.png

查看smaps进程内存使用信息

使用以下命令,查看

cat /proc/[pid]/smaps > smaps.txt

实际物理内存使用信息,以下所示:

clipboard.png

搜索下504KB,正好是12563个,对了12563个线程,其中Rss表示实际物理内存(含共享库)92KB,Pss表示实际物理内存(按比例共享库)92KB(因为没有共享库,因此Rss==Pss),以第一个7fa69a6d1000-7fa69a74f000线性区来看,其映射了92KB的空间,第二个映射了152KB的空间。以下图所示:

clipboard.png

挑出符合条件(即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时间不多。

此我向你们推荐一个架构学习交流群。交流学习群号:575745314 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多
图片描述

总结

查找Bug的时候,现场信息越多越好,同时定位Bug必需要有实质性的证据。例如内存泄露就要用你推测出的模型进行定量分析。在定量和实际对不上的时候,深挖下去,你会发现不同的风景!

相关文章
相关标签/搜索