实战 - 分析java项目线上内存泄漏内存溢出频繁GC的原因

Posted java叶新东老师

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实战 - 分析java项目线上内存泄漏内存溢出频繁GC的原因相关的知识,希望对你有一定的参考价值。

前言

有些人傻傻分不清内存泄漏和内存溢出的区别,这里简单做个科普

  • 内存溢出:就是内存不够用了,对象需要的内存大小大于你分配的堆大小,内存溢出最常见的错误就是OutOfMemoryError,简称OOM;
  • 内存泄漏:对象用完之后没被垃圾回收器(GC)回收,既然没被回收,那么这个对象就会一直占用着内存空间,这就是内存泄漏。内存泄漏的最终结果就是会导致内存溢出。因为对象一直占用,久而久之,一直叠加到超过最大堆内存时,就会导致OOM。

本次分析内存泄漏的工具主要有2个,一个是arthas,另一个是jdk自带的工具jmap,关于这2个工具的用法,可以参考我之前写的2篇文章:

模拟内存泄漏

下列的java代码是一个模拟线上的内存泄漏的代码,这段代码的业务逻辑是从数据库中读取信用数据,套用模型,并把结果进行记录和传输;

package com.gc;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 从数据库中读取信用数据,套用模型,并把结果进行记录和传输
 *
 * 启动时加入以下参数 : -Xms200M -Xmx200M -XX:+UseParallelGC  -XX:+PrintGC  -XX:+HeapDumpOnOutOfMemoryError
 * 发现启动后会频繁GC,最后导致OOM(OutOfMemoryError)
 */

public class T15_FullGC_Problem01 {

    private static class CardInfo {
        BigDecimal price = new BigDecimal(0.0);
        String name = "张三";
        int age = 5;

        Date birthdate = new Date();

        public void m() {}
    }

    private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(50,
            new ThreadPoolExecutor.DiscardOldestPolicy());

    /**
     * main方法
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        executor.setMaximumPoolSize(50);
        // 为什么是死循环?因为在生产环境中会有源源不断的数据需要处理,我们无法模拟线上环境, 所以用死循环代替;
        for (;;){
            modelFit();
            Thread.sleep(100);
        }
    }

    private static void modelFit(){
        List<CardInfo> taskList = getAllCardInfo();
        taskList.forEach(info -> {
            // do something
            executor.scheduleWithFixedDelay(() -> {
                //do sth with info
                info.m();

            }, 2, 3, TimeUnit.SECONDS);
        });
    }

    private static List<CardInfo> getAllCardInfo(){
        List<CardInfo> taskList = new ArrayList<>();

        for (int i = 0; i < 100; i++) {
            CardInfo ci = new CardInfo();
            taskList.add(ci);
        }

        return taskList;
    }
}

启动

启动时加入参数-Xms200M -Xmx200M -XX:+UseParallelGC -XX:+PrintGC -XX:+HeapDumpOnOutOfMemoryError,这段代码运行后,老年代的内存占用会慢慢升高,待内存占用到达顶峰时,会频繁Full GC(回收老年代垃圾),直到撑爆内存,最后会导致OOM异常:Exception in thread "pool-1-thread-1" java.lang.OutOfMemoryError: GC overhead limit exceeded;也就是内存溢出;

运行监控

运行一段时间后,可以看到一直不停地full GC,并且有些线程的内存已经溢出报出OOM错误;
在这里插入图片描述

解决方案:使用arthas

在用arthas 的dashboard命令看一下内存使用情况,这一看,我的天,老年代的内存已经使用了98.64%,并且已经进行了3914次的GC,也就是说Full GC进行了3914次清理都没清掉那些垃圾;
在这里插入图片描述
到这时候我们就已经确定发现了内存泄漏,接下来的工作就是要找到是那些顽固的对象没被清理调,然后在做出相应的调整;

首先要分析堆内存有哪些对象,这里使用到一个arthas的工具:heapdump,这个命令类似jdk的jmap,使用arthas导出堆转储文件命令;

[arthas@28747]$ heapdump --live /Users/mac/Downloads/dump.hprof
Dumping heap to /Users/mac/Downloads/dump.hprof ...
Heap dump file created

导出后是一个二进制文件,这个文件直接打开看到的是乱码的,所以我们需要借助一些工具,这边有2个选择,用jhatjvisualVM,因为jhat用的不多,所以我们用大家常用的jvisualVM.

使用jvisual VM加载堆转储文件dump.hprof后如下图:
在这里插入图片描述
由此结果可以看到,Date对象和Bigdecimal对象一直无法回收,而每一个定时任务就会创建一个Date对象和Bigdecimal对象,到现在为止已经有55万个了,因为只增不减,撑爆内存是迟早的事。

解决方案二:使用jmap

先使用jps命令找到正在运行的java进程id

macdeMacBook-Pro:Downloads mac$ jps
24240 App
29410 Launcher
29411 T15_FullGC_Problem01
2851 
29414 Jps
29031 Main

我们运行的类为T15_FullGC_Problem01,对应的进程id为29411,记住这个进程id;

接着用jmap命令查看这个进程id,看看内存占用排行前十的对象有哪些:

jmap -histo:live 29411| head -10
  • -histo:live :表示只查看存活的对象
  • head - 10 :这是linux自带的命令,表示查看头部前十行内容;

运行后,结果和上面的jvisual分析结果差不多,Date对象和Bigdecimal对象都是在CardInfo方法里面的。所以排行最高的就是这三个;

macdeMacBook-Pro:Downloads mac$ jmap -histo:live 29411| head -10

 num     #instances         #bytes  class name
----------------------------------------------
   1:        447100       32191200  java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask
   2:        447126       17885040  java.math.BigDecimal
   3:        447100       14307200  com.gc.T15_FullGC_Problem01$CardInfo
   4:        447100       10730400  java.util.Date
   5:        447100       10730400  java.util.concurrent.Executors$RunnableAdapter
   6:        447100        7153600  com.gc.T15_FullGC_Problem01$$Lambda$2/664223387
   7:             1        2396432  [Ljava.util.concurrent.RunnableScheduledFuture;

为什么DateBigdecimal对象没被回收

是因为modelFit()方法出现了问题,

    private static void modelFit(){
        List<CardInfo> taskList = getAllCardInfo();
        taskList.forEach(info -> {
            // do something
            executor.scheduleWithFixedDelay(() -> {
                //do sth with info
                info.m();
            }, 2, 3, TimeUnit.SECONDS);
        });
    }

仔细看这个方法内的代码,关于这段代码可以有2种解释,

1、taskList链接着info对象

infotaskList中的元素,每个info元素都taskList这个对象所引用着,每次定时任务执行完后,线程内的对象都会被垃圾回收器清理掉,但是info这个对象不属于定时任务线程内的对象,所以没被清理掉;按理说taskList内的所有对象都遍历完了之后,应该会将taskList给清除掉,但是taskList还有个别元素在线程中,他们之间的引用还在,既然有引用,也就自然不会被清理;引用关系如下图在这里插入图片描述

2、线程引用这info对象

infotaskList中的元素,每个info元素都taskList这个对象所引用着,这个定时任务executor.scheduleWithFixedDelay(() -> { }); 花括号的内容其实是在另一个域里面了,虽然info.m()这个方法已经执行完了,但是执行完后线程并没有被回收,因为线程的核心线程数和最大线程数都设置为50,所以线程执行完后一直在那挂着,既然线程还没回收,线程中GC Roots对info对象的引用就一直在,既然引用还在,垃圾回收器就不会回收被引用的对象;引用关系如下图
在这里插入图片描述

解决方案

要解决这个问题,就得解决引用的问题,让对象引用随着线程的执行完毕而清理掉,所以只需要修改modelFit()方法为以下代码即可解决问题。以下这段代码,taskList 在线程内执行,一旦线程执行完后taskList也会随着线程一起被回收掉,另外taskList内的所有CardInfo也会被回收,紧接着DateBigdecimal对象就没有引用了,也会被垃圾回收器回收掉,到这里也就解决内存泄漏问题。

  private static void modelFit(){
       executor.scheduleWithFixedDelay(() -> {
           List<CardInfo> taskList = getAllCardInfo();
           taskList.forEach(info -> {
               //do sth with info
               info.m();

            });
       }, 2, 3, TimeUnit.SECONDS);
   }

以上是关于实战 - 分析java项目线上内存泄漏内存溢出频繁GC的原因的主要内容,如果未能解决你的问题,请参考以下文章

实战 - 分析java项目线上内存泄漏内存溢出频繁GC的原因

记一次线上内存溢出问题排查过程

Java核心实操:内存溢出 实战内存泄漏实战

jvm调优篇 > CPU占用率高内存泄漏内存溢出查错教程

java内存溢出怎么解决?

Java线上故障排查