macOS上使用gperftools定位Java内存泄漏问题

这几天在排查一个堆外内存泄漏的问题时看到很多人都提到了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
复制代码

图1 进程占用的全部内存

从图中可以看到进程占用的内存远远大于我们所配的,很明显这里发生了内存泄漏。那么我们就来看看怎么使用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
复制代码

图2 heap-profiler启动配置

运行程序,可以在日志中看到heap-profiler开始跟踪内存分配,默认的采样速率是每分配100M。

图3 heap-profiler日志

在/tmp目录下也可以看到heap-profiler输出的日志。

图4 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文件了。图片比较大,这里也只截取一部分。

图5 内存分配链路

从图上可以看到内存分配的调用栈被转化为多条调用链路,最终都指向AllocMem进行内存分配,并且内存占比高的链路还被贴心的加粗。

注:如果输出pdf的时候碰到以下错误,则需要安装对应的依赖

dot: not found    需要安装graphviz
brew install graphviz

ps2pdf: command not found    需要安装ghostscript
brew install ghostscript
复制代码

四、总结

可以看到gperftools确实是一款排查内存泄漏的神器,为我们提供了堆外内存的监测能力。只可惜它本身是一款为C++设计的工具,很多其他的功能在Java下好像都无法使用。这次先研究到这里,以后再看看还有哪些功能能在Java下使用的。

官方文档

猜你喜欢

转载自juejin.im/post/5ef98f3d5188252e98363336