1. 程式人生 > 程式設計 >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

macOS上使用gperftools定位Java記憶體洩漏問題及解決方案

圖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

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下無法得到完整的監控資訊。但是我們仍然可以通過第四列找到 _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檔案了。圖片比較大,這裡也只擷取一部分。

macOS上使用gperftools定位Java記憶體洩漏問題及解決方案

圖5 記憶體分配鏈路

從圖上可以看到記憶體分配的呼叫棧被轉化為多條呼叫鏈路,最終都指向AllocMem進行記憶體分配,並且記憶體佔比高的鏈路還被貼心的加粗。

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

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

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

總結

到此這篇關於macOS上使用gperftools定位Java記憶體洩漏問題的文章就介紹到這了,更多相關gperftools定位Java記憶體洩漏內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!