目录
一. 背景
二. 内存泄露及原因
三. 常见堆内内存泄露的原因
四. 避免内存泄露的一些事项
五. 常见发生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<MessageEvent> 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文件太大无法直接导入内存。简单的解决方案是,在测试环境配置小一点压测即现。