macOS上使用gperftools定位Java内存泄漏问题及解决方案
作者:ponlanby 发布时间:2023-03-02 11:42:38
这几天在排查一个堆外内存泄漏的问题时看到很多人都提到了gperftools这个神器,想要尝试一下结果发现它对macOS的支持不太友好。而且大多数教程是针对C++的,里面的一通编译链接的操作看得我个Java仔眼花缭乱的。所以我在这里整理一份mac和Java版的使用教程,免得大家再来踩坑了。
一、简介
gperftools是google提供的一套分析工具,包括堆内存检测heap-profiler,内存泄漏分析工具heap-checker和CPU性能监测工具cpu-profiler。众所周知堆外内存的泄漏是很难追踪的,使用MAT等dump分析工具也只能从堆中最大或者最多的对象入手去分析发生泄漏的地方。而gperftools将malloc的调用替换为它自己的tcmalloc,从而统计所有内存分配的行为,帮助我们更快的定位到发生泄漏的地方。
二、安装
直接用homebrew安装就可以了。
brew install gperftools
三、使用gperftools定位内存泄漏
1.示例程序
我们使用下面这段代码来模拟一个Native Memory泄漏的场景,这段代码使用native方法分配内存并且默认使用SoftReference持有其引用,因此如果有大量对象存活在堆中又没有触发Full GC的话就会导致他们持有的Native Memory一直不被释放,最终耗尽物理机的内存。
代码地址
public class NativeMemoryLeakDemo {
public static void main(String[] args) throws IOException, FontFormatException {
while (true) {
test();
}
}
private static void test() throws IOException, FontFormatException {
Resource resource = new ClassPathResource("font/font.ttf");
Font rawFont = Font.createFont(Font.TRUETYPE_FONT, resource.getFile());
Font usedFont = rawFont.deriveFont(Font.PLAIN, 30);
BufferedImage bufferedImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = bufferedImage.createGraphics();
g2.setFont(usedFont);
g2.drawString("hello world", 16, 35);
}
}
我们先使用如下的VM参数运行一段时间(Java8)
-XX:CMSInitiatingOccupancyFraction=80
-XX:CompressedClassSpaceSize=528482304
-XX:InitialHeapSize=3221225472
-XX:MaxDirectMemorySize=536870912
-XX:MaxHeapSize=3221225472
-XX:MaxMetaspaceSize=536870912
-XX:MaxNewSize=1157627904
-XX:MetaspaceSize=536870912
-XX:NewSize=1157627904
-XX:SurvivorRatio=8
从图中可以看到进程占用的内存远远大于我们所配的,很明显这里发生了内存泄漏。那么我们就来看看怎么使用gperftools提供的heap-profiler工具定位到是哪里发生的内存泄漏。
2.使用heap_profiler定位内存泄漏的位置
1) 使用tcmalloc替换malloc
打开bash_profile
vi ~/.bash_profile
指定tcmalloc库的路径并将其加入PATH中
export DYLD_INSERT_LIBRARIES=<gperftools_lib_path>/lib/libtcmalloc_and_profiler.dylib
其中<gperftools_lib_path>是gperftools在机器上的安装位置,例如我是用homebrew安装在/usr/local/Cellar/gperftools/2.7/下的,那我的路径就是
export DYLD_INSERT_LIBRARIES=/usr/local/Cellar/gperftools/2.7/lib/libtcmalloc_and_profiler.dylib
保存并生效配置(需要重启IDE)
source ~/.bash_profile
注:这里替换掉malloc并不会运行heap-profiler,然而由于添加环境变量之后任何人都可以启动heap-profiler,因此Google不建议在生产环境配置。
2) 监控内存分配
在Idea里导入或创建我们的示例程序,在运行设置里添加heap-profiler运行的环境变量
HEAPPROFILE=<heap_output_path>
<heap_output_path>是heap文件的输出地址。例如要将结果输出到tmp文件夹下的memTrack文件中,就是
HEAPPROFILE=/tmp/memTrack
运行程序,可以在日志中看到heap-profiler开始跟踪内存分配,默认的采样速率是每分配100M。
在/tmp目录下也可以看到heap-profiler输出的日志。
3) 分析输出
heap-profiler使用pprof将结果转换成多种格式,这里分别介绍下txt和pdf的输出
输出txt
选取最后一次的采样记录memTrack.0026.heap,将其转换成txt文件后输出到~/HeapFile文件夹下
pprof $JAVA_HOME/bin/java --text /tmp/memTrack.0026.heap > ~/HeapFile/memTrack.txt
结果比较大,这里截取Java部分的输出结果
Total: 2544.9 MB
2541.9 99.9% 99.9% 2541.9 99.9% 0x00007fff6f5bb1bd
0.0 0.0% 100.0% 298.4 11.7% _JavaMain
0.0 0.0% 100.0% 0.0 0.0% _Java_com_apple_eawt_Application_nativeInitializeApplicationDelegate
0.0 0.0% 100.0% 0.0 0.0% _Java_java_awt_image_BufferedImage_initIDs
0.0 0.0% 100.0% 0.0 0.0% _Java_java_awt_image_ColorModel_initIDs
0.0 0.0% 100.0% 0.0 0.0% _Java_java_awt_image_Raster_initIDs
0.0 0.0% 100.0% 0.0 0.0% _Java_java_awt_image_SampleModel_initIDs
0.0 0.0% 100.0% 0.0 0.0% _Java_java_io_UnixFileSystem_checkAccess
0.0 0.0% 100.0% 0.1 0.0% _Java_java_io_UnixFileSystem_getBooleanAttributes0
0.0 0.0% 100.0% 0.3 0.0% _Java_java_lang_ClassLoader_00024NativeLibrary_load
0.0 0.0% 100.0% 0.1 0.0% _Java_java_lang_ClassLoader_defineClass1
0.0 0.0% 100.0% 0.1 0.0% _Java_java_lang_ClassLoader_findBootstrapClass
0.0 0.0% 100.0% 0.0 0.0% _Java_java_lang_Class_forName0
0.0 0.0% 100.0% 0.2 0.0% _Java_java_lang_System_initProperties
0.0 0.0% 100.0% 0.0 0.0% _Java_java_net_Inet6Address_init
0.0 0.0% 100.0% 0.0 0.0% _Java_java_net_NetworkInterface_init
0.0 0.0% 100.0% 0.0 0.0% _Java_java_net_PlainSocketImpl_initProto
0.0 0.0% 100.0% 0.0 0.0% _Java_java_net_PlainSocketImpl_socketConnect
0.0 0.0% 100.0% 0.9 0.0% _Java_java_util_zip_Inflater_inflateBytes
0.0 0.0% 100.0% 0.2 0.0% _Java_java_util_zip_Inflater_init
0.0 0.0% 100.0% 0.0 0.0% _Java_java_util_zip_ZipFile_getEntry
0.0 0.0% 100.0% 0.4 0.0% _Java_java_util_zip_ZipFile_open
0.0 0.0% 100.0% 0.0 0.0% _Java_sun_awt_CGraphicsEnvironment_registerDisplayReconfiguration
0.0 0.0% 100.0% 0.5 0.0% _Java_sun_awt_image_BufImgSurfaceData_initRaster
0.0 0.0% 100.0% 0.1 0.0% _Java_sun_font_CFontManager_loadNativeDirFonts
0.0 0.0% 100.0% 0.0 0.0% _Java_sun_font_StrikeCache_freeIntMemory
0.0 0.0% 100.0% 0.4 0.0% _Java_sun_font_T2KFontScaler_createScalerContextNative
0.0 0.0% 100.0% 764.7 30.0% _Java_sun_font_T2KFontScaler_getGlyphImageNative
0.0 0.0% 100.0% 0.0 0.0% _Java_sun_font_T2KFontScaler_initIDs
0.0 0.0% 100.0% 1751.7 68.8% _Java_sun_font_T2KFontScaler_initNativeScaler
0.0 0.0% 100.0% 0.0 0.0% _Java_sun_java2d_SurfaceData_initIDs
0.0 0.0% 100.0% 0.0 0.0% _Java_sun_java2d_loops_GraphicsPrimitiveMgr_initIDs
0.0 0.0% 100.0% 0.4 0.0% _Java_sun_java2d_opengl_CGLGraphicsConfig_getOGLCapabilities
0.0 0.0% 100.0% 0.0 0.0% _Java_sun_java2d_opengl_OGLRenderQueue_flushBuffer
可以看到第一行是整个程序占用的总内存,后面按照调用栈的顺序记录了每个方法的内存使用情况(单位: MB)
第一列是使用的Direct Memory
第四列是进程以及所有被它调用的方法所占用的总内存
第二列和第五列分别是第一列和第四列的内存占进程总内存的百分比
第三列是第二列数据的一个累加
由于gperftools是C++下的工具,可以看到在Java下无法得到完整的监控信息。但是我们仍然可以通过第四列找到 _Java_sun_font_T2KFontScaler_initNativeScaler 这个方法占用了最多的内存,查看代码可以看到这个方法是被native关键字修饰的,说明很可能这里分配的内存没有被JVM回收。去搜索一下就能查到确实是这里分配的内存被Font2D对象持有最终造成了泄漏。
输出pdf
pprof还支持将统计结果图形化输出到pdf,方便我们更直观的找到占用最多内存的地方。这里同样用memTrack.0026.heap,将其转换成pdf格式后输出到~/HeapFile文件夹下
pprof $JAVA_HOME/bin/java --pdf /tmp/memTrack.0026.heap > ~/HeapFile/memTrack.pdf
之后就可以在~/HeapFile下看到生成的pdf文件了。图片比较大,这里也只截取一部分。
从图上可以看到内存分配的调用栈被转化为多条调用链路,最终都指向AllocMem进行内存分配,并且内存占比高的链路还被贴心的加粗。
注:如果输出pdf的时候碰到以下错误,则需要安装对应的依赖
dot: not found 需要安装graphviz
brew install graphviz
ps2pdf: command not found 需要安装ghostscript
brew install ghostscript
官方文档
来源:https://juejin.im/post/5ef98f3d5188252e98363336


猜你喜欢
- 在Android studio实现简易计算器App并实现加减乘除功能,供大家参考,具体内容如下结果activity_main.xml<
- Android Fragment 动态创建Fragment是activity的界面中的一部分或一种行为。可以把多个Fragment组合到一个
- Spring框架是由于软件开发的复杂性而创建的。Spring使用的是基本的JavaBean来完成以前只可能由EJB完成的事情。然而,Spri
- 本文实例讲述了C#接口在派生类和外部类中的调用方法。分享给大家供大家参考,具体如下:C#的接口通过interface关键字进行创建,在接口中
- 1.第一种方式采用System.Net.Dns的GetHostAddress的方式,具体请看代码:/// <summary> &
- 接收参数的方式:1.HttpServletRequest方式接收public ModelAndView test1(HttpServletR
- 系统启动过程图: Framework层所有的Service都是运行在SystemServer进程中;SystemServer进程
- 使用Java的方式配置Spring我们现在要完全不使用Spring的xml配置,全权使用Java来配置Spring!JavaConfig是S
- 自动生成的代码报错解决办法:把自动xml文件中自动生成的二级缓存注释掉来源:https://blog.csdn.net/weixin_447
- 本文实例讲述了Java排序算法总结之希尔排序。分享给大家供大家参考。具体分析如下:前言:希尔排序(Shell Sort)是插入排序的一种。是
- 本文实例讲述了C#生成随机ArrayList的方法。分享给大家供大家参考。具体实现方法如下:public static void Rando
- # 看题目是不是很绕,这个我也不知道怎么才能更简单的表达了# 先看代码:public class Common {public static
- 这里使用 Maven 项目管理工具构建项目初始化项目打开 Intellij IDEA,点击 Create New Project选择 Mav
- Jackson反序列化遇到的问题最近在项目中需要使用Jackson把前台转来的字符转为对象,转换过程中发生了错误,报错如下com.faste
- 简介从 Spring Boot 项目名称中的 Boot 可以看出来,Spring Boot 的作用在于创建和启动新的基于 Spring 框架
- java * 的方法总结AOP的拦截功能是由java中的 * 来实现的。说白了,就是在目标类的基础上增加切面逻辑,生成增强的目标类(该
- 前言有时线上问题我们用打日志的方式来观察错误或埋点参数,但由于这些日志如果都打出来会占用大量存储空间而且覆盖了一些有效信息,所以线上级别一般
- 本文实例为大家分享了Android仿qq分组管理的第三方库,供大家参考,具体内容如下下面先看效果 我们点击展开与折叠分组的功能在库
- 在C#中DateTime是一个包含日期、时间的类型,此类型通过ToString()转换为字符串时,可根据传入给Tostring()的参数转换
- 一、解码流程解码流程大致分为以下三个部分,以FFmpge源码下的ffmpeg\doc\examples\decode_audio.c为参考。