使用XPocket插件JConsole排查线上OOM异常案例原创
XPocket插件JConsole主要用于内存问题的排查,能够对堆中的Eden、Survivor、Old区以及堆外的Metaspace、Code Cache等区域进行观察。我们在使用JConsole时依次输入如下命令:
use jconsole@JDK
attach 目标进程的pid
在attach到目标进程后,输入help命令查看具体帮助信息,如下:
Command Definitions :
Command-Name Command-Description
class ClassLoading info
gc execute a system gc
cpu_usage cpu usage info
metaspace Metaspace
ccs Compressed Class Space
survivor PS Survivor Space
thread Thread
code_cache code cache info
runtime Runtime info
eden Ps Eden Gen
old Ps Old Gen
os os info
memory memory info
mbeans mbeans -list mbeans,mbeans objectName -list
mbeans info,mbeans objectName attrName -get
attribute value.
下面举2个实际的例子看下我们如何通过JConsole来排查问题。
1、频繁类加载引起OOM异常
动态脚本语言Groovy也会引起频繁的类加载,因为为了实现动态,Groovy语言会不停的使用新的类加载器加载类,举个例子如下:
import java.util.HashMap;
import java.util.Map;
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
public class TestMetaspaceOOM {
public static void main(String args[]) throws InterruptedException {
test();
}
public static void test() throws InterruptedException {
String text = "def eval(){}";
Map<Integer, Script> map = new HashMap<Integer, Script>();
int i = 0;
while (true) {
Thread.sleep(100);
Binding binding = new Binding();
GroovyShell shell = new GroovyShell(binding);
shell.getClassLoader().clearCache();
Script script = shell.parse(text);
map.put(i, script);
if (i % 500 == 0) {
map.clear();
}
}
}
}
通常在运行一个Java应用时,都会限制元数据区的内存大小,因为这个区通常会更稳定一些,并且元数据区的默认最大大小是不受限制的,如果频繁类加载导致元数据区出了问题,那么不受限制的使用还可能会影响到其它服务,所以我们配置如下的虚拟机选项:
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m
我们给如上的实例也加上这个选项,运行一会儿后就会出现java.lang.OutOfMemoryError错误。 大概率出现的错误是如下这样的:
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
我们在JConsole中排查这个问题,先后运行class命令3次,打印的结果如下:
// 第1次运行class命令
ClassLoading
TotalLoadedClassCount = 7635
LoadedClassCount = 7635
UnloadedClassCount = 0
// 间隔10秒左右运行第2次class命令
ClassLoading
TotalLoadedClassCount = 18419
LoadedClassCount = 18419
UnloadedClassCount = 0
// 间隔10秒左右运行第2次class命令
ClassLoading
TotalLoadedClassCount = 26074
LoadedClassCount = 26074
UnloadedClassCount = 0
可以看到的是类的总量和加载的类的总数一直在持续上涨,而卸载的数量却为0,如果LoadedClassCount属性的值一直在增长,而UnloadedClassCount属性的值几乎不增长,则基本可确认有频繁类加载操作。
除了动态脚本语言Groovy等操作经常会引起频繁的类加载,反射也会引起此类问题,甚至有时候还可能是自己自定义类加载器,并且在代码里没有限制的创建了大量的自定义类加载器,导致重复加载大量的数据。但是这类问题还是比较容易发现的,难的是需要找到对应的代码进行优化。
2、堆内存泄漏引起OOM异常
如果发生了堆内存泄漏,一般来说,首先会引起的是频繁GC,由于频繁的GC,所以会造成CPU的使用率过高,而且频繁GC后STW就会变多,服务很可能会出现大面积超时,最终可能就是报堆内存OOM了。我们在观察到OOM、频繁GC等情况后,通常都会排查堆内存的使用情况。网上的教程都是先dump文件,然后使用MAT等工具查找占用内存最大的对象等。不过OOM、频繁GC等情况并不一定是堆内存泄漏造成的,有可能是内存分配过小、或者流量陡增后超过了服务器的服务能力,导致回收的速度越不上分配内存的速度等原因造成的。所以如果你已经观察到堆内存使用异常了,那么首先应该查看下是否为内存泄漏引起的,此时可使用JConsole插件来确定内存泄漏故障:
举个例子如下:
public class OutOfMemoryError_Example1 {
public static List<byte[]> list = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
int i = 1;
while (true) {
Thread.sleep(500L);
System.out.println("执行次数:" + (i++));
// 每次需要10M的大小
list.add(new byte[1024 * 1024*10]);
}
}
}
执行的结果如下:
...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.classloading.OutOfMemoryError_Example1.main(OutOfMemoryError_Example1.java:14)
实例的错误比较明显,不过许多错误都是这个实例的复杂版,本质都是往全局容器中放数据,由于某些原因,这些数据没有被正确移除导致,或者放入数据太多的速度太快而移除的速度太慢等,导致容器及其实的数据占用了过大的堆内存。 我们可以使用JConsole来排查这样的问题,通过JConsole的memory查看内存使用情况,如下:
// 首次运行memory命令
HeapMemoryUsage
init = 130023424
used = 871670264
committed = 1245184000
max = 1836580864
NonHeapMemoryUsage
init = 2555904
used = 11554296
committed = 12517376
max = -1
// 运行gc命令手动触发GC回收
// 第2次运行memory命令
HeapMemoryUsage
init = 130023424
used = 1269868024
committed = 1707081728
max = 1836580864
NonHeapMemoryUsage
init = 2555904
used = 11628672
committed = 12517376
max = -1
// 运行gc命令手动触发GC回收
// 第3次运行memory命令
HeapMemoryUsage
init = 130023424
used = 1605047632
committed = 1891106816
max = 1891106816
NonHeapMemoryUsage
init = 2555904
used = 11706672
committed = 12517376
max = -1
可以看到堆中使用的内存used和已经提交的内存committed的数值一直在上涨,最终committed的值逼近了max的值后,如果used将committed值使用完后,commited无法再继续向操作系统申请新的内存(因为已经达到了max最大值)。最终的结果就是报出堆内存溢出的异常。
我们在运行class命令之前运行了gc命令是为了尽量保证内存中的对象都是有效引用,排除垃圾对象对memory命令打印出的数值的干扰。
JConsole能够排查Java进程内存的使用情况,特别是在排查过程中要进行多次打印,比对数值来发现问题。如果要进一步在代码级别定位问题,可以使用XPocket中的其它插件进行辅助定位。