性能文章>使用XPocket插件JConsole排查线上OOM异常案例>

使用XPocket插件JConsole排查线上OOM异常案例原创

https://a.perfma.net/img/2382850
2年前
8135113

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中的其它插件进行辅助定位。

点赞收藏
鸠摩

著有《深入解析Java编译器:源码剖析与实例详解》、《深入剖析Java虚拟机:源码剖析与实例详解》等书籍。公众号“深入剖析Java虚拟机HotSpot”作者

请先登录,查看1条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

为什么要做代码Review?

为什么要做代码Review?

一次 GDB 源码角度分析 jvm 无响应问题

一次 GDB 源码角度分析 jvm 无响应问题

日常Bug排查-集群逐步失去响应

日常Bug排查-集群逐步失去响应

浅析AbstractQueuedSynchronizer

浅析AbstractQueuedSynchronizer

13
1