1. 前言
工作中有可能遇到 java.lang.OutOfMemoryError: Java heap space 内存溢出异常, 本文提供一些内存溢出的分析及解决问题的思路.
常见异常如下:
2022-01-31 16:07:29.639 ERROR 1981 --- [http-nio-8080-exec-4] 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
2. 内存溢出的问题
解决问题之前先来分析一下为什么会出现内存溢出的问题.
有两种可能性:
一种是应用有问题, 本该回收的内存没有进行回收导致的内存溢出, 这种情况就需要修改代码了.
第二种情况则是服务器资源不够或JVM参数设置过小导致的内存溢出,这种情况需要更换服务器或修改启动参数
我们可以使用对应的工具或命令来定位到问题, 然后分析是哪种情况, 最后再解决问题.
3. 场景模拟
通过下列代码来模拟内存溢出的情况:
// 通过无限创建自定义对象模拟内存溢出的场景
@GetMapping("oom")
public void oom(){
while(true){
CustomObj customObj = new CustomObj();
}
}
/**
* @author liuboren
* @Title: 自定义对象
* @Description: 创建该对象用于模拟OOM场景
* @date 2022/1/30 16:55
*/
public class CustomObj {
// 利用numbers成员变量尽可能更快的用光内存
private int[] numbers = new int[10000000];
}
再将应用的启动JVM参数设置为 -Xms70m -Xmx70m即可.
通过访问/oom的接口, 很快程序就会报
2022-01-31 16:07:29.639 ERROR 1981 --- [http-nio-8080-exec-4] 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
4. 分析的方法
问题已经出来了, 我们可以通过一下几种方法来定位分析问题:
- 查看日志
- 使用jmap命令
- 分析堆转储文件
- 利用arthas进行分析
- 使用jstat命令
4.1 日志分析
通过查看对应的日志可以很清晰的定位到错误:
java.lang.OutOfMemoryError: Java heap space
at com.example.demo.entity.CustomObj.<init>(CustomObj.java:11) ~[demo.jar:0.0.1-SNAPSHOT]
at com.example.demo.controller.TestController.oom(TestController.java:36) ~[demo.jar:0.0.1-SNAPSHOT]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
可以看到TestController类中的oom方法,里面的CustomObj对象造成了内存溢出.
这时候查看对应的代码进行分析:
@GetMapping("oom")
public void oom(){
while(true){
CustomObj customObj = new CustomObj();
}
}
这个例子是我们使用了while(true) 无限的去创造对象, 所以造成的内存溢出, 我们修改对应的代码即可.
如果程序正常的情况下,就要考虑修改JVM启动参数调整堆空间或者将应用放到内存更大的服务器即可.
4.2 jmap
通过日志只可以定位到对应的代码位置,如果我们想看内存中到底是什么对象占用的空间比较多, 这时候就可以使用jmap命令了
使用下列命令可以查看内存中已产生对象的实例数和大小
jmap -histo pid |head -n 20
-histo参数代表所有的对象,包括已经垃圾回收掉的对象, 如果只想看目前存活的对象可以增加:live参数:
jmap -histo:live pid |head -n 20
至于head -n 20 则代表输出排名前20的数据, 如果不加这个参数那么展示的数据就太多了, 不利于排查问题.
然后看实际效果:
通过上图可以看出int 类型占了 40294040bytes 差不多38mb.这是因为我的测试类中的CustomObj对象 new 了一个int数组导致的.
**
* @author liuboren
* @Title: 自定义对象
* @Description: 创建该对象用于模拟OOM场景
* @date 2022/1/30 16:55
*/
public class CustomObj {
private int[] numbers = new int[10000000];
}
使用jmap命令可以快速的查看内存中的对象的实例及占用的大小, 但是缺点就是显示的不是那么直观, 并且如果应用重启了那么也就无法查看了.
所以为了避免这种情况,可以通过生成堆转储文件来进行分析.
4.3 堆转储文件分析
刚刚说了使用jmap进行内存分析的缺点, 现在看看如何使用堆转储文件
生成堆转储文件有3中方式:
- 启动时添加 JVM参数
-XX:+HeapDumpOnOutOfMemoryError参数表示当JVM发生OOM时,自动生成DUMP文件。
- 使用jmap
jmap -dump:live,format=b,file=heap.bin <pid>
- 使用arthas
heapdump
生成堆转储文件之后, 需要dump到本地进行分析
分析堆转储文件的三种方式:
- jhat
jhat -port 8000 java_pid2162.hprof
jhat默认端口是7000, 如果有端口占用的情况, 可以通过 -port 参数替换默认端口
- visualVm
JVisualVm
- Eclipse Memory Analyzer
下面看看实际的效果:
- jhat
利用jhat分析堆转储文件的可视化效果不是那么友好, 不重点介绍了, 下图是可以通过查询语句来显示大于50k的对象.
- VisualVm
执行JVisualVm命令启动客户端后, 导入堆转储文件:
显示基本的信息及执行错误的线程:
点击线程可以查看是执行的哪段代码:
对象的类型、实例数及大小
同样支持利用语句查询内存中的对象, 下面是查询内存中大于5mb的对象
可以看到VisualVm的显示界面是相当友好的, 并且功能十分的强大,可以查看是哪个线程执行的哪段代码,同时也可以查看对象的类型和大小. 推荐使用VisualVm
-
Eclipse Memory Analyzer
Eclipse Memory Analyzer 的功能同样很强大,就是需要额外的装一些东西, 有兴趣的朋友可以参考下面的链接 , 不多做介绍了:
链接 -
使用对转储文件的缺点
堆转储文件的优势是展示界面友好, 并且不会因为应用重启而丢失, 但是它最大的问题就是慢, 因为随着应用的运行对转储文件的体积也在不断增加, 小则几g大则几十上百g. 无论是将文件dump到本地然还是进行分析都是非常耗时的.
4.4 arthas
Arthas 是Alibaba开源的Java诊断工具. 非常好用, 不了解的同学自行百度.
下面正文
使用arthas的 jvm和 dashboard命令 可以查看jvm的情况, 并且使用heapdump也可以生成堆转储文件
jvm命令可以看到 使用的jvm 参数 、使用的垃圾回收器、垃圾回收的时间、新生代老年代的空间、堆内存的使用情况等等
启动参数:
垃圾回收情况:
内存使用情况:
dashboard 可以看到线程执行情况及内存中各个区域的大小及使用情况:
使用heapdump命令可以生成堆转储文件
4.5 jstat
jstat也是jdk自带的小工具, 功能非常的强大,可以查看垃圾会回收的次数及时间, 查看新生代老年代的剩余空间等等.
命令如下:
jstat -gcutil pid 1000
1000是毫秒数,代表每1000毫秒输出一次
我使用jstat命令主要是查看应用的full gc的情况, 如果出现频繁的full gc 这时候就很有必要对程序进行调优了.
频繁full gc 的两个调整思路:
- 尝试调整新生代和老年代的比例, 将新生代的比例调大,这样做的原因在于动态对象年龄判定的机制(同年龄的对象的大小超过整个Survivor区的一半,大于等于这个年龄的对象都会被放入老年代)
- 尝试更换垃圾回收器(例如将cms更换为 g1)
总结
以上就是我个人的一些分析解决OOM的一些经验之谈, 如果应用发生了OOM的异常, 我们可以通过以下几个步骤尝试分析解决:
- 查看日志, 可以定位到对应的代码段, 然后进行分析是否是应用有问题, 有的话进行修改
- 通过jmap命令查看内存中的对象是什么占用的比较多,是否有需要优化的对象
- 添加对应的jvm参数可以在发生oom的时候生成堆转储文件, 然后使用对应的工具或命令来进行分析, 这样做的好处在于就算应用重启了依然有迹可循,然后解决问题
- 使用arthas进行分析. arthas不得不说非常的强大, 线上问题排查的利器. 谁用谁知道.
- 使用jstat分析gc的情况和耗时,如果有频繁的full gc,也许要进行解决