内存溢出问题需要怎么分析?如果每天内存只泄露几十M,测试环境和本地开发环境根本难察觉到,但是最终的后果就是每隔几周生产环境就宕机一次。无脑的增加环境内存只是延长了宕机周期,从根本解决问题才能一劳永逸。

首先说下JAVA的对象回收机制 (内容引用《深入理解JAVA虚拟机》一书,结尾附下载地址)

JAVA的对象回收是根据可达性分析来判断对象是否存活,这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所 走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连 (用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如 图3-1所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达 的,所以它们将会被判定为是可回收的对象。

在Java语言中,可作为GC Roots的对象包括下面几种: 

虚拟机栈(栈帧中的本地变量表)中引用的对象。 

方法区中类静态属性引用的对象。 

方法区中常量引用的对象。 

本地方法栈中JNI(即一般说的Native方法)引用的对象。

根据这个基本原理,我们可以确定一点:内存泄露的原因是被GC Roots引用的对象一直在增加。 

内存泄露的DEMO(一个真实案例的简化版)

真实案例是这样:程序启动后每隔一个星期就会发生一次内存溢出,因为每次内存泄露很小,导致本地基本无法复现。最后分析完发现是一个第三方JAR中存在内存泄露的问题。

Demo代码代码如下: 

public class JvmOOM {
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new BugObject();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    public static class BugObject {
        private byte[] buff = new byte[50 * 1024 * 1024];
        public BugObject() {
            Runtime.getRuntime().addShutdownHook(new Thread(() -> BugObject.this.destroy()));
        }
        public void destroy() {
            System.out.println("destroy");
        }
    }
}

每隔1秒,新建一个BugObject对象,BugObject对象构造函数里会有个程序退出的钩子,为了加速内存泄露,BugObject里面放了个50M的字节数组。

我们运行Demo后,隔一会可以获取到下面的异常信息

OOM具体分析过程

我们整个分析过程只用了一个JDK默认的工具:jvisualvm,默认在JDK安装目录的bin目录下;JDK加入环境变量后,在终端直接运行jvisualvm就能打开。

选择需要分析的JAVA进程,双击后出现监控界面,因为DEMO中每隔1秒内存泄露50M,所以可以很明显的看到堆内存一直的增加;每次泄露很少的内存短时间内很难从这里看出来有内存泄露。

下面是定位问题的具体步骤:

第一步:执行垃圾回收后,堆Dump

第二步:隔10秒(这个时间一般需要根据内存泄露发生时间来设定,如果1个星期泄露一次,可以隔几个小时)重复第一步

第三步:重复第二步(一般是根据内存泄露爆发时长来,时间越长,就多重复几次第二步),这样可以减少干扰

第四步:执行完成上面几步后,我们就会有多个heapdump,选中一个heapdump,切换到类视图,选择“与另一个堆转储进行比较”。

然后根据类的大小排序,很快就可以发现byte[]占用的内存,在6秒内增加了200M,这时候基本可以判断就是有byte[]对象没有被回收,既然没有被回收肯定是被GC Roots引用了。

双击类名进入实例视图,并且展开对象引用关系

根据引用关系可以定位到最顶层的是被 ApplicationShutdownHooks 类的静态字段 hooks 字段引用。根据JAVA的对象回收机制的原理,明细可以知道这是个GC Root。

所以定位到DEMO代码中的这行代码有问题

Runtime.getRuntime().addShutdownHook(new Thread(() -> BugObject.this.destroy()));

这行代码会使BugObject对象被GC Root对象一直引用,导致对象无法被回收。 这里的分析步骤直接使用了jvisualvm 来获取堆dump,并不适合在线上环境处理。

生产环境的分析方式

线上可以使用jmap命令导出堆dump,下载到本地后再使用jvisualvm来分析

  1. 使用jps -l 获取到JAVA程序进程
  2. 通过 jmap -dump:format=b,file=[file] [pid] 导出dump
  1. 将服务器上生成的dump文件下载到本地,使用jvisualvm对比dump文件来分析问题

载入堆,再对比堆

和使用之前的方式分析结果一致

这里用到的命令行工具都是JDK自带的,一般无需额外安装。

关注公众号,输入“JAVA虚拟机”获取《深入理解JAVA虚拟机》一书的电子版。 

打赏
拒绝纸上谈兵,从JVM原理到实例分析内存溢出 – Javaer必备技巧

发表评论