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

目录

一. 背景

二. 内存泄露及原因

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

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

五. 常见发生OOM的日志

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

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

八. 总结

一. 背景

1.在第一章节(JVM系列-java内存模型)中我们知道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 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 {

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 final static EventFactory EVENT_FACTORY = () -> new MessageEvent();

}

}

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


定位:

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

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

 1>.安装mat

  打开应用市场       

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

2>.切换到mat视图

3>.导入dump文件

4>.分析

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

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

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

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

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

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

下面我们来具体看下

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

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

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

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

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

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

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

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系列-堆内内存泄露案例分析解决)