引言
以往碰到内存泄漏等问题的时候,都是使用eclipse下的mat(memory analyze tool)进行内存分析,但是写代码用习惯idea的现代化界面,再去看mat的界面总感觉怪怪的。终于,idea在最近几个版本也推出了内置的内存分析工具,正好前几天某个服务可能发生了内存泄漏,找了个事情不多的下午,开始实战分析。
分析完之后很简单就找到了内存泄漏的代码所在,问题也比较简单明显,共享变量使用完无法回收空间,在业务量达到一定程度之后服务自然跑不动了。下面写个简单的demo模拟分析过程。
服务结构
D:.
│ OverflowApplication.java
│
├─endpoint
│ MainAction.java
│
└─service
MemoryLeakService.java
UnluckyService.java
其中MemoryLeakService就是内存泄露的元凶,UnluckyService则是很不幸的一个触发oom的业务类。
@Service
public class MemoryLeakService {
/**
* 共享变量,内存泄漏主要原因
*/
private List<String> sharedStrs;
/**
* 随机数生成
*/
private final Random seed = new Random();
private void init() {
sharedStrs = new ArrayList<>();
}
/**
* 方法执行完,没有清空共享变量,导致引用对象无法回收
*
* @param size
* @return
*/
public List<String> extract(Integer size) {
// 方法开始时清空变量
init();
IntStream.range(0, size).forEach(i -> sharedStrs.add(seed.nextLong() + "" + seed.nextLong()));
return sharedStrs;
}
}
MemoryLeakService定义了共享变量sharedStrs,在每次进行业务操作的时候清空队列,再重新填充元素,方法运行结束没有回收空间,导致这部分空间一直被占用。
@Service
public class UnluckyService {
/**
* 随机数生成
*/
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的写法很正常,但是当他想申请空间的时候,如果两个service加起来总共使用的空间超过jvm设置的最大堆内存,整个服务就GG了。
接下来启动这个服务,设置最大堆内存为256m,开启内存溢出自动生成dump文件
-XX:+HeapDumpOnOutOfMemoryError -Xmx256m
先后调用两个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
内存分析
idea的内存分析工具在profiler工具栏,打开工具栏之后再点击右边的打开文件,找到生成的hprof文件即可载入
也可以直接通过idea的file->open,打开hprof文件。文件越大,载入的越慢,冲上一杯coffe,耐心等待即可。
内存分析结果
简单介绍一下各个窗口的功能,来自官网机翻,原文请见Analyze the memory snapshot | IntelliJ IDEA Documentation (jetbrains.com)
快照的左侧显示应用程序中的类列表、每个类有多少活实例、所有实例的浅大小和保留大小。
- Shallow:分配用于存储对象本身的内存大小。它不包括此对象引用的对象的大小。
- Retained:对象的浅大小与其保留对象的浅大小之和(对象仅从该对象引用)。换句话说,保留的大小是通过对该对象进行垃圾回收可以回收的内存量。
快照的右边部分有几个选项卡,允许您计算和显示以下信息:
- Biggest Objects选项卡按其保留大小排序,列出了保留大部分内存的对象。这些对象表示为支配树根。此选项卡可以帮助您查找由单个对象引起的内存泄漏。
- GC Roots选项卡显示了具有相应垃圾收集器根对象的类列表。该信息是在快照拍摄时无法垃圾收集的所有对象的概述。例如,查看哪个类加载器在应用程序服务器中占用了最多的内存,这可能很有用。
- Merged Paths选项卡按类显示分组对象,并显示到保留它们的支配器对象的路径。这些信息有助于理解为什么保留特定类的实例。
- Summary选项卡显示常规信息,例如,线程的总大小、实例数量和堆栈跟踪。
- Packages选项卡按包显示所有对象的细分。这可以帮助您快速确定哪个子系统占用了最多的内存消耗和可能的内存泄漏。
分析结果
从分析结果来看,在最大对象视图里可以直接看到MemoryLeakService是当前占用内存最多的对象,服务是在运行到UnluckyService第25行时发生了oom,就此两个信息,可以得出结论:MemoryLeakService存在内存无法回收,UnluckyService运行时内存不足。接下来就只需要去查看MemoryLeakService源码,分析内存泄漏原因即可。
想要修复这个bug也很简单,把共享变量改为方法域临时变量,或在每次使用之后清空队列(有线程安全问题需要解决)。
总结
代码虐我千百遍,我待代码如初恋。不管是内存泄漏还是内存溢出,还是别的千奇百怪的诡异问题,都有其或深或浅的原因,而idea的内存分析工具还提供了很多功能帮助分析排查内存相关的问题。
PS:目前发现某些低版本的idea(比如2020)分析结果可用信息不足,使用时还请升级到较高的版本,我测试使用的idea版本是2021.2.3。是否需要ultimate版本目前还不知道,社区版如果没有这个功能,也可以升级到旗舰版试试。
网友评论