Java分析线上OOM问题的工具
一、相关工具介绍1、jpsjps可以列出正在运行的虚拟机进程,并显示虚拟机执行主类名以及这些进程的本地虚拟机唯一ID(LVMID),LVMID与操作系统的进程ID(PID)是一致的/ # jps1 ExpenseApplication6370 Jps选项作用-v输出虚拟机进程启动时的JVM参数后面介绍的命令都监控的是LVMID为1的这个JVM进程2、jstatjstat是用于监视虚拟机各种运行状态
一、相关工具介绍
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
更多推荐
所有评论(0)