溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

關于macOS上使用gperftools定位Java內存泄漏的案例分析

發(fā)布時間:2020-07-01 15:54:21 來源:億速云 閱讀:638 作者:清晨 欄目:開發(fā)技術

這篇文章主要介紹關于macOS上使用gperftools定位Java內存泄漏的案例分析,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!

一、簡介

gperftools是google提供的一套分析工具,包括堆內存檢測heap-profiler,內存泄漏分析工具heap-checker和CPU性能監(jiān)測工具cpu-profiler。眾所周知堆外內存的泄漏是很難追蹤的,使用MAT等dump分析工具也只能從堆中最大或者最多的對象入手去分析發(fā)生泄漏的地方。而gperftools將malloc的調用替換為它自己的tcmalloc,從而統(tǒng)計所有內存分配的行為,幫助我們更快的定位到發(fā)生泄漏的地方。

二、安裝

直接用homebrew安裝就可以了。

brew install gperftools

三、使用gperftools定位內存泄漏

 1.示例程序

我們使用下面這段代碼來模擬一個Native Memory泄漏的場景,這段代碼使用native方法分配內存并且默認使用SoftReference持有其引用,因此如果有大量對象存活在堆中又沒有觸發(fā)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

關于macOS上使用gperftools定位Java內存泄漏的案例分析

圖1 進程占用的全部內存

從圖中可以看到進程占用的內存遠遠大于我們所配的,很明顯這里發(fā)生了內存泄漏。那么我們就來看看怎么使用gperftools提供的heap-profiler工具定位到是哪里發(fā)生的內存泄漏。

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,然而由于添加環(huán)境變量之后任何人都可以啟動heap-profiler,因此Google不建議在生產環(huán)境配置。

2) 監(jiān)控內存分配

在Idea里導入或創(chuàng)建我們的示例程序,在運行設置里添加heap-profiler運行的環(huán)境變量

HEAPPROFILE=<heap_output_path>

<heap_output_path>是heap文件的輸出地址。例如要將結果輸出到tmp文件夾下的memTrack文件中,就是

HEAPPROFILE=/tmp/memTrack

關于macOS上使用gperftools定位Java內存泄漏的案例分析

圖2 heap-profiler啟動配置

運行程序,可以在日志中看到heap-profiler開始跟蹤內存分配,默認的采樣速率是每分配100M。

關于macOS上使用gperftools定位Java內存泄漏的案例分析

圖3 heap-profiler日志

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

關于macOS上使用gperftools定位Java內存泄漏的案例分析

圖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下無法得到完整的監(jiān)控信息。但是我們仍然可以通過第四列找到 _Java_sun_font_T2KFontScaler_initNativeScaler 這個方法占用了最多的內存,查看代碼可以看到這個方法是被native關鍵字修飾的,說明很可能這里分配的內存沒有被JVM回收。去搜索一下就能查到確實是這里分配的內存被Font2D對象持有最終造成了泄漏。

輸出pdf

pprof還支持將統(tǒng)計結果圖形化輸出到pdf,方便我們更直觀的找到占用最多內存的地方。這里同樣用memTrack.0026.heap,將其轉換成pdf格式后輸出到~/HeapFile文件夾下

pprof $JAVA_HOME/bin/java --pdf /tmp/memTrack.0026.heap > ~/HeapFile/memTrack.pdf

之后就可以在~/HeapFile下看到生成的pdf文件了。圖片比較大,這里也只截取一部分。

關于macOS上使用gperftools定位Java內存泄漏的案例分析

圖5 內存分配鏈路

從圖上可以看到內存分配的調用棧被轉化為多條調用鏈路,最終都指向AllocMem進行內存分配,并且內存占比高的鏈路還被貼心的加粗。

注:如果輸出pdf的時候碰到以下錯誤,則需要安裝對應的依賴

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

ps2pdf: command not found  需要安裝ghostscript
brew install ghostscript

以上是關于macOS上使用gperftools定位Java內存泄漏的案例分析的所有內容,感謝各位的閱讀!希望分享的內容對大家有幫助,更多相關知識,歡迎關注億速云行業(yè)資訊頻道!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI