一、相关工具介绍

1、jps

jps可以列出正在运行的虚拟机进程,并显示虚拟机执行主类名以及这些进程的本地虚拟机唯一ID(LVMID),LVMID与操作系统的进程ID(PID)是一致的

/ # jps
1 ExpenseApplication
6370 Jps
选项作用
-v输出虚拟机进程启动时的JVM参数

后面介绍的命令都监控的是LVMID为1的这个JVM进程

2、jstat

jstat是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据

选项作用
-gc监视Java堆状况,包括Eden区、2个Survivor区、老年代、永久代(元空间)等的容量,已用空间,垃圾收集时间合计等信息
-gcutil监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比
-gccause与-gcutil功能一样,但是会额外输出导致上一次垃圾收集产生的原因

1)、-gcutil

/ # jstat -gcutil 1
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT    CGC    CGCT     GCT   
  0.00  26.05  62.02  74.18  97.48  92.14    201    3.339     8    1.659     -        -    4.998

各参数的含义如下:

S0,年轻代中第一个Survivor区已使用的占当前容量百分比
S1,年轻代中第二个Survivor区已使用的占当前容量百分比
E,年轻代中Eden区已使用的占当前容量百分比
O,老年代已使用的占当前容量百分比
M,元空间已使用的占当前容量百分比
YGC,从应用程序启动到采样时年轻代中GC次数
YGCT,从应用程序启动到采样时年轻代中GC所用时间(s)
FGC,从应用程序启动到采样时老年代GC次数
FGCT,从应用程序启动到采样时老年代GC所用时间(s)
GCT,从应用程序启动到采样时GC用的总时间(s)

2)、-gccause

/ # jstat -gccause 1
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT    CGC    CGCT     GCT    LGCC                 GCC                 
  0.00  13.71  51.79  76.64  96.91  90.88    213    3.485     8    1.647     -        -    5.132 Allocation Failure   No GC  

LGCC:导致上一次垃圾收集产生的原因

Allocation Failure:本次引起GC的原因是因为在年轻代中没有足够的空间能够存储新的数据了

3、jmap

jmap命令用于生成堆转储快照

如果不使用jmap命令,要想获取堆转储快照,可以添加JVM启动参数:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/dump.hprod

OOM时自动生成堆转储快照,指定堆转储快照文件放置路径

/ # jmap -dump:format=b,file=/app/dump.hprod 1
Heap dump file created
  • jmap -dump是会dump所有的对象,不关心是否可达
  • jmap -dump:live只会dump存活的对象,即可以从GcRoot可达的对象
4、VisualVM

从JDK9开始,VisualVM不再集成在Oracle JDK中,需要单独下载安装

下载地址:https://visualvm.github.io/download.html

导入使用jmap命令生成的堆转储快照

在这里插入图片描述

在这里插入图片描述

VisualVM的堆转储分析功能并不是很强大,只能查看类使用内存的直方图,无法有效跟踪内存使用的引用关系

5、MAT

下载地址:https://www.eclipse.org/mat/downloads.php

使用MAT分析OOM问题,一般可以按照以下思路进行:

  • 通过支配树功能或直方图功能查看消耗内存最大的类型,来分析内存泄露的大概原因
  • 查看那些消耗内存最大的类型、详细的对象明细列表,以及它们的引用链,来定位内存泄露的具体点
  • 配合查看对象属性的功能,可以脱离源码看到对象的各种属性的值和依赖关系,帮助我们理清程序逻辑和参数
  • 辅助使用查看线程栈来看OOM问题是否和过多线程有关,甚至可以在线程栈看到OOM最后一刻出现异常的线程

1)、用MAT打开后先进入到是概览信息界面

在这里插入图片描述

2)、工具栏的第二个按钮可以打开直方图,直方图按照类型进行分组,列出了每个类有多少个实例以及占用的内存

在这里插入图片描述

可以看到,字节数组占用内存最多,对象数量也很多,结合第二位的String类型对象数量也很多,可以猜出程序可能是被字符串占满了内存,导致OOM

在String上点击右键,选择List objects->with incoming references,就可以列出所有实例,以及每个实例的引用关系链

在这里插入图片描述

在这里插入图片描述

String被ArrayList的elementData字段引用,ArrayList又被FooService的data字段引用

Retained Heap(深堆)代表对象本身和对象关联的对象占用的内存,Shallow Heap(浅堆)代表对象本身占用的内存。比如,ArrayList的elementData对象本身只有24字节,但是其所有关联的对象占用了大量的内存。这些就可以说明,肯定有哪里在不断向这个List中添加String数据,导致了OOM

3)、工具栏第三个按钮进入支配树界面

这个界面会按照对象保留的Retained Heap倒序直接列出占用内存最大的对象

在这里插入图片描述

4)、工具栏的第五个按钮,打开线程视图,首先看到的就是一个名为main的线程,展开后发现了FooService

在这里插入图片描述

分析OOM问题的示例代码

@SpringBootApplication
public class OOMApplication implements CommandLineRunner {
    @Autowired
    private FooService fooService;

    public static void main(String[] args) {
        SpringApplication.run(OOMApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        //程序启动后,不断调用FooService.oom()方法
        while (true) {
            fooService.oom();
        }
    }
}
@Component
public class FooService {

    List<String> data = new ArrayList<>();

    public void oom() {
        //往同一个ArrayList中不断加入大小为10KB的字符串
        data.add(IntStream.rangeClosed(1, 10_000)
                .mapToObj(e -> "a")
                .collect(Collectors.joining("")));
    }
}

二、线上OOM排查基本流程

1)通过jps找到正在执行的JVM进程

2) jstat查看基本的堆内存和GC信息

3)jmap生成堆转储快照

4)使用MAT分析堆转储快照

参考:

https://time.geekbang.org/column/article/230534

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐