如何使用intellij-idea內(nèi)存分析工具排查內(nèi)存泄漏問題

引言

以往碰到內(nèi)存泄漏等問題的時(shí)候,都是使用eclipse下的mat(memory analyze tool)進(jìn)行內(nèi)存分析,但是寫代碼用習(xí)慣idea的現(xiàn)代化界面,再去看mat的界面總感覺怪怪的。終于,idea在最近幾個版本也推出了內(nèi)置的內(nèi)存分析工具,正好前幾天某個服務(wù)可能發(fā)生了內(nèi)存泄漏,找了個事情不多的下午,開始實(shí)戰(zhàn)分析。
分析完之后很簡單就找到了內(nèi)存泄漏的代碼所在,問題也比較簡單明顯,共享變量使用完無法回收空間,在業(yè)務(wù)量達(dá)到一定程度之后服務(wù)自然跑不動了。下面寫個簡單的demo模擬分析過程。

服務(wù)結(jié)構(gòu)

D:.
│  OverflowApplication.java
│
├─endpoint
│      MainAction.java
│
└─service
        MemoryLeakService.java
        UnluckyService.java

其中MemoryLeakService就是內(nèi)存泄露的元兇,UnluckyService則是很不幸的一個觸發(fā)oom的業(yè)務(wù)類。

@Service
public class MemoryLeakService {
    /**
     * 共享變量,內(nèi)存泄漏主要原因
     */
    private List<String> sharedStrs;

    /**
     * 隨機(jī)數(shù)生成
     */
    private final Random seed = new Random();

    private void init() {
        sharedStrs = new ArrayList<>();
    }

    /**
     * 方法執(zhí)行完,沒有清空共享變量,導(dǎo)致引用對象無法回收
     *
     * @param size
     * @return
     */
    public List<String> extract(Integer size) {
        // 方法開始時(shí)清空變量
        init();
        IntStream.range(0, size).forEach(i -> sharedStrs.add(seed.nextLong() + "" + seed.nextLong()));
        return sharedStrs;
    }

}

MemoryLeakService定義了共享變量sharedStrs,在每次進(jìn)行業(yè)務(wù)操作的時(shí)候清空隊(duì)列,再重新填充元素,方法運(yùn)行結(jié)束沒有回收空間,導(dǎo)致這部分空間一直被占用。

@Service
public class UnluckyService {
    /**
     * 隨機(jī)數(shù)生成
     */
    private final Random seed = new Random();

    public List<String> extract(Integer size) {
        List<String> innerStrs = new ArrayList<>();
        IntStream.range(0, size).forEach(i -> innerStrs.add(seed.nextLong() + "" + seed.nextLong()));
        return innerStrs;
    }
}

UnluckyService的寫法很正常,但是當(dāng)他想申請空間的時(shí)候,如果兩個service加起來總共使用的空間超過jvm設(shè)置的最大堆內(nèi)存,整個服務(wù)就GG了。
接下來啟動這個服務(wù),設(shè)置最大堆內(nèi)存為256m,開啟內(nèi)存溢出自動生成dump文件

-XX:+HeapDumpOnOutOfMemoryError -Xmx256m

先后調(diào)用兩個service的方法,得到了oom文件

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid47508.hprof ...
Heap dump file created [269511232 bytes in 0.971 secs]
2023-01-09 10:30:10.809 ERROR 47508 --- [nio-8080-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space] with root cause

java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:3332) ~[na:1.8.0_40]
    at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:137) ~[na:1.8.0_40]
    at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:121) ~[na:1.8.0_40]
    at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:673) ~[na:1.8.0_40]
    at java.lang.StringBuilder.append(StringBuilder.java:214) ~[na:1.8.0_40]
    at kitsuna.overflow.service.UnluckyService.lambda$extract$0(UnluckyService.java:25) ~[classes/:na]
    at kitsuna.overflow.service.UnluckyService$$Lambda$606/963461306.accept(Unknown Source) ~[na:na]
    at java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:110) ~[na:1.8.0_40]
    at java.util.stream.IntPipeline$Head.forEach(IntPipeline.java:557) ~[na:1.8.0_40]
    at kitsuna.overflow.service.UnluckyService.extract(UnluckyService.java:25) ~[classes/:na]
    at kitsuna.overflow.endpoint.MainAction.unluckyExtract(MainAction.java:30) ~[classes/:na]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_40]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_40]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_40]
    at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_40]
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[spring-web-5.3.24.jar:5.3.24]
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150) ~[spring-web-5.3.24.jar:5.3.24]
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[spring-webmvc-5.3.24.jar:5.3.24]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) ~[spring-webmvc-5.3.24.jar:5.3.24]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[spring-webmvc-5.3.24.jar:5.3.24]
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.3.24.jar:5.3.24]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1071) ~[spring-webmvc-5.3.24.jar:5.3.24]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:964) ~[spring-webmvc-5.3.24.jar:5.3.24]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.24.jar:5.3.24]
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.24.jar:5.3.24]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:670) ~[tomcat-embed-core-9.0.70.jar:4.0.FR]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.24.jar:5.3.24]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:779) ~[tomcat-embed-core-9.0.70.jar:4.0.FR]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[tomcat-embed-core-9.0.70.jar:9.0.70]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.70.jar:9.0.70]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.70.jar:9.0.70]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.70.jar:9.0.70]


Process finished with exit code 130

內(nèi)存分析

idea的內(nèi)存分析工具在profiler工具欄,打開工具欄之后再點(diǎn)擊右邊的打開文件,找到生成的hprof文件即可載入


也可以直接通過idea的file->open,打開hprof文件。文件越大,載入的越慢,沖上一杯coffe,耐心等待即可。
內(nèi)存分析結(jié)果

簡單介紹一下各個窗口的功能,來自官網(wǎng)機(jī)翻,原文請見Analyze the memory snapshot | IntelliJ IDEA Documentation (jetbrains.com)
快照的左側(cè)顯示應(yīng)用程序中的類列表、每個類有多少活實(shí)例、所有實(shí)例的淺大小和保留大小。

  • Shallow:分配用于存儲對象本身的內(nèi)存大小。它不包括此對象引用的對象的大小。
  • Retained:對象的淺大小與其保留對象的淺大小之和(對象僅從該對象引用)。換句話說,保留的大小是通過對該對象進(jìn)行垃圾回收可以回收的內(nèi)存量。

快照的右邊部分有幾個選項(xiàng)卡,允許您計(jì)算和顯示以下信息:

  • Biggest Objects選項(xiàng)卡按其保留大小排序,列出了保留大部分內(nèi)存的對象。這些對象表示為支配樹根。此選項(xiàng)卡可以幫助您查找由單個對象引起的內(nèi)存泄漏。
  • GC Roots選項(xiàng)卡顯示了具有相應(yīng)垃圾收集器根對象的類列表。該信息是在快照拍攝時(shí)無法垃圾收集的所有對象的概述。例如,查看哪個類加載器在應(yīng)用程序服務(wù)器中占用了最多的內(nèi)存,這可能很有用。
  • Merged Paths選項(xiàng)卡按類顯示分組對象,并顯示到保留它們的支配器對象的路徑。這些信息有助于理解為什么保留特定類的實(shí)例。
  • Summary選項(xiàng)卡顯示常規(guī)信息,例如,線程的總大小、實(shí)例數(shù)量和堆棧跟蹤。
  • Packages選項(xiàng)卡按包顯示所有對象的細(xì)分。這可以幫助您快速確定哪個子系統(tǒng)占用了最多的內(nèi)存消耗和可能的內(nèi)存泄漏。

分析結(jié)果

從分析結(jié)果來看,在最大對象視圖里可以直接看到MemoryLeakService是當(dāng)前占用內(nèi)存最多的對象,服務(wù)是在運(yùn)行到UnluckyService第25行時(shí)發(fā)生了oom,就此兩個信息,可以得出結(jié)論:MemoryLeakService存在內(nèi)存無法回收,UnluckyService運(yùn)行時(shí)內(nèi)存不足。接下來就只需要去查看MemoryLeakService源碼,分析內(nèi)存泄漏原因即可。
想要修復(fù)這個bug也很簡單,把共享變量改為方法域臨時(shí)變量,或在每次使用之后清空隊(duì)列(有線程安全問題需要解決)。


總結(jié)

代碼虐我千百遍,我待代碼如初戀。不管是內(nèi)存泄漏還是內(nèi)存溢出,還是別的千奇百怪的詭異問題,都有其或深或淺的原因,而idea的內(nèi)存分析工具還提供了很多功能幫助分析排查內(nèi)存相關(guān)的問題。
PS:目前發(fā)現(xiàn)某些低版本的idea(比如2020)分析結(jié)果可用信息不足,使用時(shí)還請升級到較高的版本,我測試使用的idea版本是2021.2.3。是否需要ultimate版本目前還不知道,社區(qū)版如果沒有這個功能,也可以升級到旗艦版試試。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容