引言
以往碰到內(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,耐心等待即可。

簡單介紹一下各個窗口的功能,來自官網(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ū)版如果沒有這個功能,也可以升級到旗艦版試試。