JVM 问题分析思路

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.(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的数据, 如果不加这个参数那么展示的数据就太多了, 不利于排查问题.

然后看实际效果:

JVM 问题分析思路_第1张图片

通过上图可以看出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中方式:

  1. 启动时添加 JVM参数
-XX:+HeapDumpOnOutOfMemoryError参数表示当JVM发生OOM时,自动生成DUMP文件。
  1. 使用jmap
jmap -dump:live,format=b,file=heap.bin 

  1. 使用arthas
heapdump

生成堆转储文件之后, 需要dump到本地进行分析

分析堆转储文件的三种方式:

  1. jhat
jhat -port 8000 java_pid2162.hprof

jhat默认端口是7000, 如果有端口占用的情况, 可以通过 -port 参数替换默认端口

  1. visualVm
JVisualVm
  1. Eclipse Memory Analyzer

下面看看实际的效果:

  • jhat

利用jhat分析堆转储文件的可视化效果不是那么友好, 不重点介绍了, 下图是可以通过查询语句来显示大于50k的对象.

JVM 问题分析思路_第2张图片

  • VisualVm

执行JVisualVm命令启动客户端后, 导入堆转储文件:
JVM 问题分析思路_第3张图片
显示基本的信息及执行错误的线程:
JVM 问题分析思路_第4张图片
点击线程可以查看是执行的哪段代码:
JVM 问题分析思路_第5张图片
对象的类型、实例数及大小
JVM 问题分析思路_第6张图片
同样支持利用语句查询内存中的对象, 下面是查询内存中大于5mb的对象
JVM 问题分析思路_第7张图片

可以看到VisualVm的显示界面是相当友好的, 并且功能十分的强大,可以查看是哪个线程执行的哪段代码,同时也可以查看对象的类型和大小. 推荐使用VisualVm

  • Eclipse Memory Analyzer

    Eclipse Memory Analyzer 的功能同样很强大,就是需要额外的装一些东西, 有兴趣的朋友可以参考下面的链接 , 不多做介绍了:
    链接

  • 使用对转储文件的缺点

堆转储文件的优势是展示界面友好, 并且不会因为应用重启而丢失, 但是它最大的问题就是, 因为随着应用的运行对转储文件的体积也在不断增加, 小则几g大则几十上百g. 无论是将文件dump到本地然还是进行分析都是非常耗时的.

4.4 arthas

Arthas 是Alibaba开源的Java诊断工具. 非常好用, 不了解的同学自行百度.

官方文档

下面正文

使用arthas的 jvm和 dashboard命令 可以查看jvm的情况, 并且使用heapdump也可以生成堆转储文件

jvm命令可以看到 使用的jvm 参数 、使用的垃圾回收器、垃圾回收的时间、新生代老年代的空间、堆内存的使用情况等等

启动参数:
JVM 问题分析思路_第8张图片
垃圾回收情况:
JVM 问题分析思路_第9张图片
内存使用情况:
JVM 问题分析思路_第10张图片

dashboard 可以看到线程执行情况及内存中各个区域的大小及使用情况:
JVM 问题分析思路_第11张图片

使用heapdump命令可以生成堆转储文件

4.5 jstat

jstat也是jdk自带的小工具, 功能非常的强大,可以查看垃圾会回收的次数及时间, 查看新生代老年代的剩余空间等等.
命令如下:

jstat -gcutil  pid  1000  

1000是毫秒数,代表每1000毫秒输出一次

JVM 问题分析思路_第12张图片

我使用jstat命令主要是查看应用的full gc的情况, 如果出现频繁的full gc 这时候就很有必要对程序进行调优了.

频繁full gc 的两个调整思路:

  1. 尝试调整新生代和老年代的比例, 将新生代的比例调大,这样做的原因在于动态对象年龄判定的机制(同年龄的对象的大小超过整个Survivor区的一半,大于等于这个年龄的对象都会被放入老年代)
  2. 尝试更换垃圾回收器(例如将cms更换为 g1)

总结

以上就是我个人的一些分析解决OOM的一些经验之谈, 如果应用发生了OOM的异常, 我们可以通过以下几个步骤尝试分析解决:

  1. 查看日志, 可以定位到对应的代码段, 然后进行分析是否是应用有问题, 有的话进行修改
  2. 通过jmap命令查看内存中的对象是什么占用的比较多,是否有需要优化的对象
  3. 添加对应的jvm参数可以在发生oom的时候生成堆转储文件, 然后使用对应的工具或命令来进行分析, 这样做的好处在于就算应用重启了依然有迹可循,然后解决问题
  4. 使用arthas进行分析. arthas不得不说非常的强大, 线上问题排查的利器. 谁用谁知道.
  5. 使用jstat分析gc的情况和耗时,如果有频繁的full gc,也许要进行解决

参考连接

jstat命令详解

堆转储文件分析

arthas官方文档

你可能感兴趣的:(JVM 问题分析思路)