5.JVM系列-堆内内存泄露案例分析解决

Posted 爱吃糖果

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了5.JVM系列-堆内内存泄露案例分析解决相关的知识,希望对你有一定的参考价值。

目录

一. 背景

二. 内存泄露及原因

三. 常见堆内内存泄露的原因

四. 避免内存泄露的一些事项

五. 常见发生OOM的日志

六. 定位&解决堆内内存泄露引起的OOM

七. 导出dump文件出现的一些问题

八. 总结

一. 背景

在第一章节中我们知道JVM堆(heap)是划分在JVM内存模型中,还有一部分内存区域堆外内存(Direct Memory)不在JVM内存模型中,通常我们写的业务代码发生的内存泄露区域可能在heap中,而NIO引起的大部分情况在Direct Memory中。  注意是大部分,而真正决定发生泄露的区域取决于申请内存的方式。

java可以通过new关键字和ByteBuffer.allocate()两种方式来申请堆内内存,通过这两种方式申请发生泄露都在此区域。

java中也提供了ByteBuffer.allocateDirect()和通过反射获取unsafe实例然后通过unsafe.allocateMemory(size)两种方式来直接申请内存,(ByteBuffer.allocateDirect()底层还是对unsafe.allocateMemory(size)的调用),这块内存不归JVM管理也就说JVM不会回收这部分内存区域。往往在高并发NIO请求和NIO mmap如果没有及时释放也会产生内存不足。

本章节主要说明堆内内存,下章节堆外。


二. 内存泄露及原因

 简单来说,内存泄露是指使用完的对象应该被JVM垃圾回收机制回收,但是由于存在占着引用未释放的情况无法被回收的现象。

什么情况下不会被释放?也就是说,我看代码怎么取判断创建的对象会不会被JVM回收?

JVM会从GC Root开始向下搜索,如果一个对象到GC Roots没有任何引用链,则说明此对象不可用。以CMS和G1为例,垃圾收集器首先会标记出GC ROOT,第二阶段从GC ROOT开始依次向下并发的标记,第三阶段再重新标记第二阶段引用有变化对象,只有没有被标记到的对象才可以被回收。

我们来看看GC Root都有哪些:整理自(Help - Eclipse Platform)

我把这个分为两类,一类是与我们的程序直接相关,还有一类是间接相关

直接相关:

1>.局部变量

2>.静态变量

3>.方法入参

4>.被monitor的对象,如synchronized(Object)

5>.未终止的线程对象

6>.系统类,即:通过系统类加载器加载的类。

间接:

1>.本地局部变量

2>.本地静态变量


三. 常见堆内内存泄露的原因

1. 由spring托管的对象的集合属性。

2. 静态集合对象被某个不会被回收的对象引用。

3. 查询未命中条件load全表数据。

4. disruptor中ringBuffer导致的泄露,并发下log4j2异步模式底层使用disruptor容易产生泄楼。

5.并发下动态创建类,如动态代理创建类。

6.一些缓存无限制的使用,比如guava cache


四. 避免内存泄露的一些事项

1. spring托管的对象的集合尤其是map每次使用完clear调。

2. 创建集合对象生命周期尽可能的短,也就是说在真正需要的地方才去创建并add数据,而不是在外层add好透传进入。

3. 一次性不要加载太多数据,如果真有场景,则要带着id分页。

4. 并发场景日志输出要精简。

5.并发下动态创建类为了提升性能是不会加锁,所以要考虑保证线程安全。

6.缓存的使用一定要加自动过期和淘汰机制。


五. 常见发生OOM的日志

1>.heap 不足引起OOM

2>.Java.lang.OutOfMemeoryError:GC overhead limit exceeded

通过统计GC时间来预测是否要OOM了,提前抛出异常,防止OOM发生。Sun 官方对此的定义是:“并行/并发回收器在GC回收时间过长时会抛出OutOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存。用来避免内存过小造成应用不能正常工作。

总之也是因为内存不足。

3>.其他内存区域的不一一列举,基本都类似。


六. 定位&解决堆内内存泄露引起的OOM

0>.演示程序 模拟创建大集合,存储MessageEvent对象

JVM参数:

-Xmx1024m -Xms1024m  -XX:+UseConcMarkSweepGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/dump -XX:+PrintGC -XX:+PrintGCCause -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC  -XX:+PrintGCApplicationStoppedTime  -XX:+PrintPromotionFailure  -Xloggc:/tmp/g1test.log

代码:

package com.mbj.mbjtest.disruptor;
import static
com.lmax.disruptor.RingBuffer.createSingleProducer;
import
java.util.concurrent.CountDownLatch;
import
java.util.concurrent.ExecutorService;
import
java.util.concurrent.Executors;
import
com.lmax.disruptor.*;
import
org.slf4j.Logger;
import
org.slf4j.LoggerFactory;
import
com.lmax.disruptor.util.PaddedLong;
public class
DisruptorTest {
   protected static final Logger log = LoggerFactory.getLogger(DisruptorTest.class);
   private static final int
THREAD_NUMS = 200;
   private static final int
BUFFER_SIZE = 2048 * 2048 * 8;
   private static final long
NUMS = 1000_000_00L;
   public static void
main(String[] args) throws InterruptedException {
       RingBuffer<MessageEvent> ringBuffer =
               createSingleProducer(MessageEvent.EVENT_FACTORY,
                       
BUFFER_SIZE,new BlockingWaitStrategy());
       
ExecutorService executors = Executors.newFixedThreadPool(10);
       
SequenceBarrier sequenceBarrier = ringBuffer.newBarrier();
       
MessageMutationEventHandler[] handlers = new MessageMutationEventHandler[THREAD_NUMS];
       
BatchEventProcessor<?>[] batchEventProcessors = new BatchEventProcessor[THREAD_NUMS];
       for
(int i = 0; i < THREAD_NUMS; i++) {
           handlers[i] = new MessageMutationEventHandler();
           
batchEventProcessors[i] = new BatchEventProcessor<>(ringBuffer, sequenceBarrier, handlers[i]);
           
ringBuffer.addGatingSequences(batchEventProcessors[i].getSequence());
       
}
       CountDownLatch latch = new CountDownLatch(THREAD_NUMS);
       for
(int i = 0; i < THREAD_NUMS; i++) {
           long n = batchEventProcessors[i].getSequence().get() + NUMS;
           
handlers[i].reset(latch, n);
           
executors.submit(batchEventProcessors[i]);
       
}

       for (long i = 0; i < NUMS; i++) {
           long sequence = ringBuffer.next();
           
ringBuffer.get(sequence).setValue(i);
           
ringBuffer.publish(sequence);
       
}
       latch.await();
       
executors.shutdown();
   
}

   public static final class MessageMutationEventHandler implements
           
EventHandler<MessageEvent> {
       private final PaddedLong value = new PaddedLong();
       private long
count;
       private
CountDownLatch latch;
       public
MessageMutationEventHandler() {

       }

       public long getValue() {
           return value.get();
       
}

       public void reset(final CountDownLatch latch, final long expectedCount) {
           value.set(0L);
           this
.latch = latch;
           
count = expectedCount;
       
}

       @Override
       
public void onEvent(final MessageEvent event, final long sequence,
                           final boolean
endOfBatch) throws Exception {
           System.out.println(event.getValue());
           
value.set(event.getValue());
           if
(count == sequence) {
               latch.countDown();
           
}
       }
   }

   public static final class MessageEvent {
       private long value;
       private
String name;
       public
MessageEvent() {
           name = new String("test");
       
}

       public long getValue() {
           return value;
       
}

       public void setValue(long value) {
           this.value = value;
       
}

       public String getName() {
           return name;
       
}

       public void setName(String name) {
           this.name = name;
       
}

       public final static EventFactory<MessageEvent> EVENT_FACTORY = () -> new MessageEvent();
   
}
}

    GC日志:已经频繁的Full gc了

      5.JVM系列-堆内内存泄露案例分析解决

定位:

在第一章节中有说明使用jmap、jstack、或者jcmd可以分析一些简单的原因,但是想要更加准确的定位通常使用mat可以准确快速的定位泄露的位置,这里以mat为例说明。

1>.下载eclipse并安装mat插件

 1>.安装mat

  打开应用市场       

5.JVM系列-堆内内存泄露案例分析解决

搜索框输入mat,红框内容,点击install即可。

5.JVM系列-堆内内存泄露案例分析解决

    2>.切换到mat视图

5.JVM系列-堆内内存泄露案例分析解决

3>.导入dump文件

5.JVM系列-堆内内存泄露案例分析解决


4>.分析

当内存泄露导致内存溢出,我们应该怎么分析?或者说拿到dump文件分析什么?

dump文件解压后都有什么东西?

5.JVM系列-堆内内存泄露案例分析解决

简单来说dump文件就是导出那一刻内存中的一个快照,主要包含内存中都装的什么东西,都有哪些线程,当时的线程日志

而当泄露之后,我们需要分析的也就是这些东西。我们需要知道内存中到底存的什么东西?如果看到某个对象几十万个甚至更多,那多半就发生泄露了。我们知道大部分对象都是朝生夕灭,

所有我们重点关注这些大对象,结合我们的代码分析该不该同一时间存在这么多对象,顺着这个思路基本能够定位是否发生泄露。

通过上面基本能够分析出泄露的对象,拿到泄露的对象接下来就是要分析泄露的代码位置,如果是发生OOM那就更加容易定位了,我们只需要通过线程日志即可定位到抛错的代码行。

下面我们来具体看下

1>.看看内存中到底装的什么东西,

Histogram列出每个类产生的实例数量和占用的内存大小默认的大小单位是 Bytes,可以看到MessageEvent有1800W个,Shallow Heap(对象本身占据的内存大小)=450M,Retained Heap(当前对象大小+当前对象可直接或间接引用到的对象的大小总和)=900M,而我配置的max heap=1G,加上JVM本身存储最终超过了max从而引起FULL GC最终OOM。

5.JVM系列-堆内内存泄露案例分析解决

知道泄露的对象,其次是定位改对象被引用的地方,可以利用dominator_tree查看,dominator_tree可以看到对象与其引用关系的树状结构,可以定位到对象到GCroot的引用关系。

可以看出,messageEvent对象被Ringbuffer引用,在main线程中创建。

5.JVM系列-堆内内存泄露案例分析解决

其次呢我们也看到String类也占了很多内存,对于这种也是重点分析的对象

5.JVM系列-堆内内存泄露案例分析解决

这里可以看到所有引用其的对象,按照内存大小分析前面的。

5.JVM系列-堆内内存泄露案例分析解决

可以看出String对象最终都是由MessageEvent对象引用的。

5.JVM系列-堆内内存泄露案例分析解决  

2>.接下来通过线程日志分析,OOM的代码行

从下面的线程日志可以看出,发生了OOM,也可以通过此错误栈定位到抛错的代码行。


七. 导出dump文件出现的一些问题

若在执行命令导出dump出现以下问题: Caused by: sun.jvm.hotspot.utilities.AssertionFailure: can not get class data for XXX

如:Caused by: sun.jvm.hotspot.utilities.AssertionFailure: can not get class data for java/lang/UNIXProcess$$Lambda?

Caused by: sun.jvm.hotspot.utilities.AssertionFailure: can not get class data for java/time/temporal/TemporalQueries$$Lambda$1380x00000007c0c3b028

原因:是JDK的bug,导出过程会占用大量内存OS主动kill掉,导致导出失败,升级大8u72以上即可解决

八. 总结

堆内存泄露只要有dump文件基本都可以定位,但是有时候dump文件太大无法直接导入内存。简单的解决方案是,在测试环境配置小一点压测即现。


以上是关于5.JVM系列-堆内内存泄露案例分析解决的主要内容,如果未能解决你的问题,请参考以下文章

内存泄漏定位以及解决

源码分析|Handler内存泄漏分析及解决

Android性能优化:手把手带你全面了解 内存泄露 & 解决方案

ThreadLocal内存泄露原因分析

Android中使用Thread造成内存泄露的分析和解决

Andfroid 内存溢出与内存泄漏的简单分析与解决