当前位置: 首页 > 编程笔记 >

macOS上使用gperftools定位Java内存泄漏问题及解决方案

冷翼
2023-03-14
本文向大家介绍macOS上使用gperftools定位Java内存泄漏问题及解决方案,包括了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

官方文档

总结

到此这篇关于macOS上使用gperftools定位Java内存泄漏问题的文章就介绍到这了,更多相关gperftools定位Java内存泄漏内容请搜索小牛知识库以前的文章或继续浏览下面的相关文章希望大家以后多多支持小牛知识库!

 类似资料:
  • 本文向大家介绍PHPExcel内存泄漏问题解决方法,包括了PHPExcel内存泄漏问题解决方法的使用技巧和注意事项,需要的朋友参考一下 使用 PHPExcel 来生成 excel 文档是比较消耗内存的,有时候可能会需要通过一个循环来把大数据切分成若干个小的 excel 文档保存来避免内存耗尽。 然而 PHPExcel 存在 circular references 的情况(貌似在最新的 1.6.5

  • 问题内容: 我有一个Grails应用程序,该应用程序完成了相当不错的域对象创建和销毁工作,而且它似乎以非常非常快的速度耗尽了PermGen空间。我已经进行了通常的调整(将PermGen调整为256M,启用了类GC,等等),但是没有骰子。 有人愿意推荐一些(可能是免费或非常便宜的)工具来解决Groovy和/或Java中的这种内存消耗问题吗?还是您用来解决JVM内存问题的某些技术? 编辑:这是在生产模

  • 本文向大家介绍Android Handler内存泄漏详解及其解决方案,包括了Android Handler内存泄漏详解及其解决方案的使用技巧和注意事项,需要的朋友参考一下 关联篇:深入Android的消息机制源码详解-Handler,MessageQueue与Looper关系 关联篇:HandlerThread 使用及其源码完全解析 在android开发过程中,我们可能会遇到过令人奔溃的OOM异

  • 本文向大家介绍IOS 常见内存泄漏以及解决方案,包括了IOS 常见内存泄漏以及解决方案的使用技巧和注意事项,需要的朋友参考一下 IOS 常见内存泄漏以及解决方案 整理了几个内存泄漏的例子,由于转载地址已经找不到了,在这里就不一一列出来了。 1 OC和CF转化出现的内存警告 2,循环参照 A有个属性参照B,B有个属性参照A,如果都是strong参照的话,两个对象都无法释放。 这种问题常发生于把del

  • 问题内容: 我有一个Python程序,它运行一系列实验,没有打算从一个测试存储到另一个测试的数据。我的代码包含一个我完全找不到的内存泄漏(我已经查看了内存泄漏的其他线程)。由于时间限制,我不得不放弃寻找泄漏的机会,但是如果我能够隔离每个实验,该程序可能会运行足够长的时间以产生所需的结果。 在单独的线程中运行每个测试是否有帮助? 还有其他隔离泄漏影响的方法吗? 具体情况详 我的代码分为两部分:实验运

  • 本文向大家介绍详解ES6通过WeakMap解决内存泄漏问题,包括了详解ES6通过WeakMap解决内存泄漏问题的使用技巧和注意事项,需要的朋友参考一下 一、Map 1.定义 Map对象保存键值对,类似于数据结构字典;与传统上的对象只能用字符串当键不同,Map对象可以使用任意值当键。 2.语法 属性 size:返回键值对的数量。 操作方法 set(key, value):设置(新增/更新)键key的