堆外内存泄露

时间:2022-07-28 15:13:09

原文:https://mp.weixin.qq.com/s?__biz=MzIzNzQzNjEwMw==&mid=2247484816&idx=1&sn=2e60a5afffcbcec401c10a3c85c91173&chksm=e8c9e838dfbe612e8134613b37bf739d22de242482a758dcfdbb55b0beff6c819e9bbb392f9d&mpshare=1&scene=1&srcid=0825klmjxzyItztJzs2n2m9h&key=c95b8826c6a30de172494a8bb70669690c993d206f74ecfeb91ef3c1697202b2ac84e06ab5f0d304243542a6e7ea4d28a8010ed9549347c78f4ecfc5d1ebf1d2fa722829b58547cb82d90749940fcb8a&ascene=0&uin=MTIzNTQyNQ%3D%3D&devicetype=iMac+MacBookPro11%2C4+OSX+OSX+10.10.5+build(14F27)&version=11020201&pass_ticket=1xIOAZaMnNFr6urP9PDMT1mHM2mLt%2Fjb6AjksnYk%2F24%3D



   怪事年年有,最近特别多,近期遇到一个堆外内存泄露导致物理内存RES占用居高不下的问题,遇到这类问题比较少,因此将整个排查过程记录下来


现象描述


       最近1周线上服务器时不时出现RES报警(某一个服务RES占用持续飙高,高致十几个G),jstack查看现场信息也没有发现程序中存在任何线程堆栈调用异常,事态紧急,临时采取重启服务救火;


      但导致的根本原因是什么呢?必须找到根本原因,总是这么重启也不是个办法,而且!实在是有点low!


问题排查


       查问题的前提是复现问题,线上肯定是不能动了。(找死差不多)找来一台沙箱服务器,部署上异常服务,通过本地灰度压测沙箱异常服务接口使得异常服务持续处在一种高压状态,果不其然,伴着压测的持续进行,异常服务进程常驻内存RES快速增长,分分钟突破5G大关:


堆外内存泄露


        问题是复现了,怎么去定位问题出现的原因呢?


定位问题


        At first,大家都会想到的,考虑堆内存的使用情况,使用jmap命令查看进程相关的堆内存的使用情况。


堆外内存泄露


        结果,堆内存占用情况非常正常,想来也是,进程启动前jvm参数已经限制了进程的堆内存占用大小,最大不过2G


堆外内存泄露


(图中可以看到jvm设置初始内存2g,最大内存2g,年轻代1g,线程栈大小1M,持久代大小最大512M,堆内内存泄漏的情况可以基本排除)


      Then,使用jstat命令查看下进程相关的gc情况


堆外内存泄露


        可以明显的发现,S0和S1区一直未被使用,EU以及OU持续飙高,YGC的次数很少而 FGC的次数在不断增长,也就是说,对象在E区被创建后不经过S区直接送达O区,对比JVM配置MaxTenuringThreshold=0,新生代对象经过0次gc就可以直奔老年代,程序对于常驻内存的持久数据做这样的处理也很正常,但是高增不降的确有些不寻常,以防万一,仔细检查服务接口代码是否维护有大量的持久化数据,然而并没有什么收获,基本可以确定问题不是出现在服务器的代码上。排除这个原因,如无意外,应该就是堆外内存的使用的问题,


        到这里,本着不抛弃不放弃的精神,继续压测,异常服务相关进程终于寿终就寝,临死状态现场资源占用状态如下图所示:


堆外内存泄露


    注意!进程奔溃前RES占用达到21.5g!!!这是不给其他的服务活路了啊。


基本解决方案


       通过配置JVM参数我们可以对堆外内存的申请做出提前的限制,

-XX:MaxDirectMemorySize=2g 

         -------对于通过JNI申请的堆外内存做出最大值限制

-XX:+CMSPermGenSweepingEnabled 

        -------为了避免Perm区满引起的full gc,开启CMC回收perm

-XX:-OmitStackTraceInFastThrow 

        -------异常堆栈过多依然突出其完整的堆栈信息(监控用,非解决)

改完重启:程序运行正常,问题基本解决。


        BUT!JVM的配置只是对于堆外内存的使用做出了强制的限制,并没有定位到问题出现的根本原因,这是我们的风格吗,显然不是,So,我们来深入分析下堆外内存的使用情况。


        堆外内存包含线程栈,应用程序代码,NIO缓存,JNI调用,具体是哪一块引发的RES飙高,我表示完全没有头绪,对于这样一个不受到JVM管理的“非法区域,无能为力。


        然,事情总是要干的!


        查询了下资料,咨询了相关专业人员(贾老板),获知一款开源神器gperftools可以担此大任。


        What is gperftools???

        gperftools是google开发的一款非常实用的工具集,主要包括:性能优异的malloc free内存分配器tcmalloc;基于tcmalloc的堆内存检测和内存泄漏分析工具heap-profiler,heap-checker(这里我们主要用到的组件)。所谓得神器者得天下,接下来的一切就顺利成章了。


 开始:

1、安装

        下载google-perftools-1.8.2.tar.gz&libunwind-0.99-beta.tar.gz,在沙箱机器上编译安装


2、配置

        在需要监控的服务的启动脚本前加上

堆外内存泄露

        配置申请堆外内存的库连接文件和监控堆使用日志的存储目录

堆外内存泄露JAVA_OPT上添加如上的语句,在程序启动的时候运行jprofile监控程序


3、解析结果

        压测一段时间后生成多份内存监控使用日志,我们挑选最近的一份日志:

堆外内存泄露


        第一列代表这个函数调用本身直接使用了多少内存

        第二列表示第一列的百分比    

        第三列是从第一行到当前行的所有第二列之和

        第四列表示这个函数调用自己直接使用加上所有子调用使用的内存总和

        第五列是第四列的百分比


        很明显的发现引发内存异常的方法是Deflater的native方法init!!!


        喜大普奔!总算找到了罪魁祸首!事情还没有结束,凶手背后的幕后主使还没有找到,于是我们找来gperftools的助手btrace。


        接下来就是查看方法的调用链了,因为不是程序中直接编写的代码,不好debug,所以我选择使用了Btrace来解决这个问题。(有关btrace的安装以及使用请参考http://blog.csdn.net/u011630575/article/details/49404277


        通过BTrace定位代码调用方,编写代码BtracerInflater.java对init方法进行拦截


堆外内存泄露


        在沙箱上根据进程pid运行我们编写的java脚本最终定位到其调用链


堆外内存泄露


        发现在FontCreater的create方法会调用到Deflate的native init() method.剩下的就是针对这个方法的修改和优化,这里就不继续赘述。


        到此,这次的堆外内存泄漏问题结束。


参考资料

http://m.blog.csdn.net/blog/whuoyunshen88/19508075

http://itindex.net/detail/11709-perftools-内存-hbase

http://outofmemory.cn/code-snippet/1713/Btrace-usage-introduction