macOS上使用gperftools定位Java內(nèi)存泄漏問(wèn)題及解決方案
這幾天在排查一個(gè)堆外內(nèi)存泄漏的問(wèn)題時(shí)看到很多人都提到了gperftools這個(gè)神器,想要嘗試一下結(jié)果發(fā)現(xiàn)它對(duì)macOS的支持不太友好。而且大多數(shù)教程是針對(duì)C++的,里面的一通編譯鏈接的操作看得我個(gè)Java仔眼花繚亂的。所以我在這里整理一份mac和Java版的使用教程,免得大家再來(lái)踩坑了。
一、簡(jiǎn)介
gperftools是google提供的一套分析工具,包括堆內(nèi)存檢測(cè)heap-profiler,內(nèi)存泄漏分析工具h(yuǎn)eap-checker和CPU性能監(jiān)測(cè)工具cpu-profiler。眾所周知堆外內(nèi)存的泄漏是很難追蹤的,使用MAT等dump分析工具也只能從堆中最大或者最多的對(duì)象入手去分析發(fā)生泄漏的地方。而gperftools將malloc的調(diào)用替換為它自己的tcmalloc,從而統(tǒng)計(jì)所有內(nèi)存分配的行為,幫助我們更快的定位到發(fā)生泄漏的地方。
二、安裝
直接用homebrew安裝就可以了。
brew install gperftools
三、使用gperftools定位內(nèi)存泄漏
1.示例程序
我們使用下面這段代碼來(lái)模擬一個(gè)Native Memory泄漏的場(chǎng)景,這段代碼使用native方法分配內(nèi)存并且默認(rèn)使用SoftReference持有其引用,因此如果有大量對(duì)象存活在堆中又沒(méi)有觸發(fā)Full GC的話就會(huì)導(dǎo)致他們持有的Native Memory一直不被釋放,最終耗盡物理機(jī)的內(nèi)存。
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參數(shù)運(yùn)行一段時(shí)間(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
從圖中可以看到進(jìn)程占用的內(nèi)存遠(yuǎn)遠(yuǎn)大于我們所配的,很明顯這里發(fā)生了內(nèi)存泄漏。那么我們就來(lái)看看怎么使用gperftools提供的heap-profiler工具定位到是哪里發(fā)生的內(nèi)存泄漏。
2.使用heap_profiler定位內(nèi)存泄漏的位置
1) 使用tcmalloc替換malloc
打開bash_profile
vi ~/.bash_profile
指定tcmalloc庫(kù)的路徑并將其加入PATH中
export DYLD_INSERT_LIBRARIES=<gperftools_lib_path>/lib/libtcmalloc_and_profiler.dylib
其中<gperftools_lib_path>是gperftools在機(jī)器上的安裝位置,例如我是用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并不會(huì)運(yùn)行heap-profiler,然而由于添加環(huán)境變量之后任何人都可以啟動(dòng)heap-profiler,因此Google不建議在生產(chǎn)環(huán)境配置。
2) 監(jiān)控內(nèi)存分配
在Idea里導(dǎo)入或創(chuàng)建我們的示例程序,在運(yùn)行設(shè)置里添加heap-profiler運(yùn)行的環(huán)境變量
HEAPPROFILE=<heap_output_path>
<heap_output_path>是heap文件的輸出地址。例如要將結(jié)果輸出到tmp文件夾下的memTrack文件中,就是
HEAPPROFILE=/tmp/memTrack
運(yùn)行程序,可以在日志中看到heap-profiler開始跟蹤內(nèi)存分配,默認(rèn)的采樣速率是每分配100M。
在/tmp目錄下也可以看到heap-profiler輸出的日志。
3) 分析輸出
heap-profiler使用pprof將結(jié)果轉(zhuǎn)換成多種格式,這里分別介紹下txt和pdf的輸出
輸出txt
選取最后一次的采樣記錄memTrack.0026.heap,將其轉(zhuǎn)換成txt文件后輸出到~/HeapFile文件夾下
pprof $JAVA_HOME/bin/java --text /tmp/memTrack.0026.heap > ~/HeapFile/memTrack.txt
結(jié)果比較大,這里截取Java部分的輸出結(jié)果
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
可以看到第一行是整個(gè)程序占用的總內(nèi)存,后面按照調(diào)用棧的順序記錄了每個(gè)方法的內(nèi)存使用情況(單位: MB)
- 第一列是使用的Direct Memory
- 第四列是進(jìn)程以及所有被它調(diào)用的方法所占用的總內(nèi)存
- 第二列和第五列分別是第一列和第四列的內(nèi)存占進(jìn)程總內(nèi)存的百分比
- 第三列是第二列數(shù)據(jù)的一個(gè)累加
由于gperftools是C++下的工具,可以看到在Java下無(wú)法得到完整的監(jiān)控信息。但是我們?nèi)匀豢梢酝ㄟ^(guò)第四列找到 _Java_sun_font_T2KFontScaler_initNativeScaler 這個(gè)方法占用了最多的內(nèi)存,查看代碼可以看到這個(gè)方法是被native關(guān)鍵字修飾的,說(shuō)明很可能這里分配的內(nèi)存沒(méi)有被JVM回收。去搜索一下就能查到確實(shí)是這里分配的內(nèi)存被Font2D對(duì)象持有最終造成了泄漏。
輸出pdf
pprof還支持將統(tǒng)計(jì)結(jié)果圖形化輸出到pdf,方便我們更直觀的找到占用最多內(nèi)存的地方。這里同樣用memTrack.0026.heap,將其轉(zhuǎn)換成pdf格式后輸出到~/HeapFile文件夾下
pprof $JAVA_HOME/bin/java --pdf /tmp/memTrack.0026.heap > ~/HeapFile/memTrack.pdf
之后就可以在~/HeapFile下看到生成的pdf文件了。圖片比較大,這里也只截取一部分。
從圖上可以看到內(nèi)存分配的調(diào)用棧被轉(zhuǎn)化為多條調(diào)用鏈路,最終都指向AllocMem進(jìn)行內(nèi)存分配,并且內(nèi)存占比高的鏈路還被貼心的加粗。
注:如果輸出pdf的時(shí)候碰到以下錯(cuò)誤,則需要安裝對(duì)應(yīng)的依賴
dot: not found 需要安裝graphviz brew install graphviz ps2pdf: command not found 需要安裝ghostscript brew install ghostscript
總結(jié)
到此這篇關(guān)于macOS上使用gperftools定位Java內(nèi)存泄漏問(wèn)題的文章就介紹到這了,更多相關(guān)gperftools定位Java內(nèi)存泄漏內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Spring boot+CXF開發(fā)WebService Demo
這篇文章主要介紹了詳解Spring boot+CXF開發(fā)WebService Demo,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-05-05SpringBoot?SpringSecurity?JWT實(shí)現(xiàn)系統(tǒng)安全策略詳解
Spring?Security是Spring的一個(gè)核心項(xiàng)目,它是一個(gè)功能強(qiáng)大且高度可定制的認(rèn)證和訪問(wèn)控制框架。它提供了認(rèn)證和授權(quán)功能以及抵御常見(jiàn)的攻擊,它已經(jīng)成為保護(hù)基于spring的應(yīng)用程序的事實(shí)標(biāo)準(zhǔn)2022-11-11java線程池:獲取運(yùn)行線程數(shù)并控制線程啟動(dòng)速度的方法
下面小編就為大家?guī)?lái)一篇java線程池:獲取運(yùn)行線程數(shù)并控制線程啟動(dòng)速度的方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-05-05java super關(guān)鍵字知識(shí)點(diǎn)詳解
在本篇文章里小編給大家整理的是一篇關(guān)于java super關(guān)鍵字知識(shí)點(diǎn)詳解內(nèi)容,有興趣的朋友們可以參考下。2021-01-01HashMap和HashTable底層原理以及常見(jiàn)面試題
今天小編就為大家分享一篇關(guān)于HashMap和HashTable底層原理以及常見(jiàn)面試題,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-01-01詳解使用Spring Boot開發(fā)Restful程序
本篇文章主要介紹了詳解使用Spring Boot開發(fā)Restful程序,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05如何把第三方服務(wù)注冊(cè)到spring項(xiàng)目容器中
這篇文章主要為大家介紹了如何把第三方服務(wù)注冊(cè)到spring項(xiàng)目容器中,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07SpringBoot中使用HTTP客戶端工具Retrofit
這篇文章主要為大家介紹了SpringBoot中使用HTTP客戶端工具Retrofit方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06Spring實(shí)現(xiàn)控制反轉(zhuǎn)和依賴注入的示例詳解
這篇文章主要為大家詳細(xì)介紹IoC(控制反轉(zhuǎn))和DI(依賴注入)的概念,以及如何在Spring框架中實(shí)現(xiàn)它們,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-08-08