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内存泄漏内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 一次 Java 内存泄漏的排查解决过程详解

    由来 前些日子小组内安排值班,轮流看顾我们的服务,主要做一些报警邮件处理.Bug 排查.运营 issue 处理的事.工作日还好,无论干什么都要上班的,若是轮到周末,那这一天算是毁了. 不知道是公司网络广了就这样还是网络运维组不给力,网络总有问题,不是这边交换机脱网了就是那边路由器坏了,还偶发地各种超时,而我们灵敏地服务探测服务总能准确地抓住偶现的小问题,给美好的工作加点料.好几次值班组的小伙伴们一起吐槽,商量着怎么避过服务保活机制,偷偷停了探测服务而不让人发现(虽然也并不敢). 前些天我就在周末

  • java OOM内存泄漏原因及解决方法

    前言 这篇文章主要介绍了java OOM内存泄漏原因及解决方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.什么是OOM OOM,全称"Out Of Memory",翻译成中文就是"内存用完了",当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error 二.为什么会OOM.出现的原因是什么 为什么会没有内存了呢?原因不外乎有两点: ① 分配的少了:比如虚拟机本身可

  • Java中关于内存泄漏出现的原因汇总及如何避免内存泄漏(超详细版)

    Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题.内存泄漏大家都不陌生了,简单粗俗的讲,就是该被释放的对象没有释放,一直被某个或某些实例所持有却不再被使用导致 GC 不能回收.最近自己阅读了大量相关的文档资料,打算做个 总结 沉淀下来跟大家一起分享和学习,也给自己一个警示,以后 coding 时怎么避免这些情况,提高应用的体验和质量. 我会从 java 内存泄漏的基础知识开始,并通过具体例子来说明 Android 引起内存泄漏的各种原因,以

  • 解析Java的JNI编程中的对象引用与内存泄漏问题

    JNI,Java Native Interface,是 native code 的编程接口.JNI 使 Java 代码程序可以与 native code 交互--在 Java 程序中调用 native code:在 native code 中嵌入 Java 虚拟机调用 Java 的代码. JNI 编程在软件开发中运用广泛,其优势可以归结为以下几点: 利用 native code 的平台相关性,在平台相关的编程中彰显优势. 对 native code 的代码重用. native code 底层操作

  • Android性能优化之利用Rxlifecycle解决RxJava内存泄漏详解

    前言: 其实RxJava引起的内存泄漏是我无意中发现了,本来是想了解Retrofit与RxJava相结合中是如何通过适配器模式解决的,结果却发现了RxJava是会引起内存泄漏的,所有想着查找一下资料学习一下如何解决RxJava引起的内存泄漏,就查到了利用Rxlifecycle开源框架可以解决,今天周末就来学习一下如何使用Rxlifecycle. 引用泄漏的背景: RxJava作为一种响应式编程框架,是目前编程界网红,可谓是家喻户晓,其简洁的编码风格.易用易读的链式方法调用.强大的异步支持等使得R

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

    这几天在排查一个堆外内存泄漏的问题时看到很多人都提到了gperftools这个神器,想要尝试一下结果发现它对macOS的支持不太友好.而且大多数教程是针对C++的,里面的一通编译链接的操作看得我个Java仔眼花缭乱的.所以我在这里整理一份mac和Java版的使用教程,免得大家再来踩坑了. 一.简介 gperftools是google提供的一套分析工具,包括堆内存检测heap-profiler,内存泄漏分析工具heap-checker和CPU性能监测工具cpu-profiler.众所周知堆外内存的

  • java内存泄漏与内存溢出关系解析

    这篇文章主要介绍了java内存泄漏与内存溢出关系解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory: 内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光. memory leak会最终会导致out of memory!

  • Java内存泄漏问题排查与解决

    前言 Java 最牛逼的一个特性就是垃圾回收机制,不用像 C++ 需要手动管理内存,所以作为 Java 程序员很幸福,只管 New New New 即可,反正 Java 会自动回收过期的对象... 那么 Java 都自动管理内存了,那怎么会出现内存泄漏,难道 Jvm 有 bug? 不要急,且听我慢慢道来.. 1. 怎么判断可以被回收 先了解一下 Jvm 是怎么判断一个对象可以被回收.一般有两种方式,一种是引用计数法,一种是可达性分析. 引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加

  • Android Handler内存泄漏原因及解决方案

    目录: 1.须知: 主线程Looper生命周期和Activity的生命周期一致. 非静态内部类,或者匿名内部类.默认持有外部类引用. 2.原因: Handler造成内存泄露的原因.非静态内部类,或者匿名内部类.使得Handler默认持有外部类的引用.在Activity销毁时,由于Handler可能有未执行完/正在执行的Message.导致Handler持有Activity的引用.进而导致GC无法回收Activity. 3.可能造成内存泄漏 匿名内部类: //匿名内部类 Handler handl

  • Golang内存泄漏场景以及解决方案详析

    目录 1.字符串截取 2.切片截取引起子切片内存泄漏 3.没有重置丢失的子切片元素中的指针 4.函数数组传参 5.goroutine 6.定时器 1)time.After 2)timer.ticker 总结 1.字符串截取 func main() { var str0 = "12345678901234567890" str1 := str0[:10] } 以上代码,会有10字节的内存泄漏,我们知道,str0和str1底层共享内存,只要str1一直活跃,str0 就不会被回收,10字节

  • 快速定位Java 内存OOM的问题

    Java服务出现了OOM(Out Of Memory)问题,总结了一些相对通用的方案,希望能帮助到Java技术栈的同学. 某Java服务(假设PID=10765)出现了OOM,最常见的原因为: 有可能是内存分配确实过小,而正常业务使用了大量内存 某一个对象被频繁申请,却没有释放,内存不断泄漏,导致内存耗尽 某一个资源被频繁申请,系统资源耗尽,例如:不断创建线程,不断发起网络连接 画外音:无非"本身资源不够""申请资源太多""资源耗尽"几个原因.

  • 简单了解JAVA内存泄漏和溢出区别及联系

    1.内存泄漏memory leak : 是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出. 2.内存溢出 out of memory : 指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出. 3.二者的关系: 内存泄漏的堆积最终会导致内存溢出 内存溢出就是你要的内存空间超过了系统实际分配给你的空间

  • 一篇文章带你搞定JAVA内存泄漏

    目录 1.什么是内存泄漏 2.内存泄漏的原因 3.内存泄漏有哪些情况 3.1 代码中没有及时释放,导致内存无法回收. 3.2 资源未关闭造成的内存泄漏 3.3 全局缓存持有的对象不使用的时候没有及时移除,导致一直在内存中无法移除 3.4 静态集合类 3.5 堆外内存无法回收 4.内存泄漏的解决办法 5.内存问题排查 第一步 首先确认逻辑问题 第二步:分析gc是否正常执行 第三步 确认下版本新增代码的改动,尽快从代码上找出问题. 第四步:开启各种命令行和 导出 dump 各种工具分析 总结: 1.

  • redis 使用lettuce 启动内存泄漏错误的解决方案

    redis 使用 lettuce 出现 LEAK: hashedwheelTimer.release() was not called before it's garbage-collected. Enable advanced leak 内存泄漏.其实是内存不够大导致. 找到eclispe 中window->preferences->Java->Installed JRE ,点击右侧的Edit 按钮,在编辑界面中的 "Default VM Arguments "选项

随机推荐