您的位置:首页 > 其它

Netty学习之旅----源码分析Netty内存泄漏检测

2017-01-08 12:56 986 查看
1、图说Netty直接内存管理







2、Netty 直接内存的使用示例

ByteBuf buf = Unpooled.directBuffer(512);
System.out.println(buf);
// SimpleLeakAwareByteBuf(UnpooledUnsafeDirectByteBuf(ridx: 0, widx: 0, cap: 512))
// SimpleLeakAwareByteBuf是包装类,使用了装饰模式,内部维护一个UnpooledUnsafeDirectByteBuf,
// 该类与UnpooledDirectByteBuf类似。首先在创建SimpleLeakAwareByteBuf的时候,会将该引用加入到内存泄漏检测
// 的引用链中。
try {
//使用业务

} finally {
buf.release();//该方法首先调用直接内存UnpooledDirectByteBuf方法,释放所占用的堆外内存,
//然后调用leak.close方法,通知内存泄漏检测程序,该引用所指向的堆外内存已经释放,没有泄漏。
//如果 release没有调用,则当UnpooledDirectBytebuf被垃圾回收器收集号,该ByteBuf
//申请的堆外内存将再也不受应用程序所掌控了,会引起内存泄漏。
/*
*
* public boolean release() {
boolean deallocated =  super.release();
if (deallocated) {
leak.close();
}
return deallocated;
}
*/
}


3、Netty内存检测实现原理
1)首先,Netty的直接内存ByteBuf的数据结构
ByteBuf 对象内部维护一个java.nio.ByteBuffer的堆外内存。java.nio.ByteBuffer所占用的实际内存,JVM虚拟机无法直接干预,JVM虚拟机GC只能回收ByteBuf对象本身,而无法回收ByteBuf所指向的堆外内存。
2)Netty封装基本的堆外内存是用UnpooledDirectByteBuf对象,Netty在每次创建一个UnpooledDirectByteBuf时,为了能够追踪到UnpooledDirectByteBuf的垃圾回收,需要将该对象用一个虚拟引用指向它,将其注册到一条引用链中。然后需要将该引用对象与ByteBuf对象保存起来,所以Netty使用装饰模式,利用一个包装类SimpleLeakAwareByteBuf对象,将原ByteBuf包装一下,但对外表现的特性,就是一个
f6ca
ByteBuf,我们来看一下:



4、io.netty.util.ResourceLeakDetector 源码分析
4.1 内部结构详解
4.1.1 4个内存检测级别
/**
* Represents the level of resource leak detection.
*/
public enum Level {
/**
* Disables resource leak detection.
*/
DISABLED,
/**
* Enables simplistic sampling resource leak detection which reports there is a leak or not,
* at the cost of small overhead (default).
*/
SIMPLE,
/**
* Enables advanced sampling resource leak detection which reports where the leaked object was accessed
* recently at the cost of high overhead.
*/
ADVANCED,
/**
* Enables paranoid resource leak detection which reports where the leaked object was accessed recently,
* at the cost of the highest possible overhead (for testing purposes only).
*/
PARANOID
}

DISABLED : 禁用内存泄露检测
SIMPLE:默认的内存检测级别,以一个时间间隔,默认是每创建113个直接内存(堆外内存)时检测一次。
ADVANCED与PARANOID 每次产生一个堆外内存,目前这两个在Netty的实现中等价,统一用ADVANCED来实现。
4.1.2 内部属性
/** the linked list of active resources */
private final DefaultResourceLeak head = new DefaultResourceLeak(null);
private final DefaultResourceLeak tail = new DefaultResourceLeak(null);

private final ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
private final ConcurrentMap<String, Boolean> reportedLeaks = PlatformDependent.newConcurrentHashMap();

private final String resourceType;
private final int samplingInterval;
private final long maxActive;
private long active;
private final AtomicBoolean loggedTooManyActive = new AtomicBoolean();

private long leakCheckCnt;

ResourceLeakDetector内部维护一个双端链表,维护所有指向堆外内存的引用对象链(ResourceLeak对象链,ResourceLeak的实现类,继承虚拟引用PhantomReference),head,与tail是虚拟节点。
refQueue 引用队列,该引用队列正存放的是ResoureLeak对象链,代表着这里面的引用对象所指向的对象已被垃圾回收。按照常理,如果该ResourceLeak对象,也同时存在于上面的双端链表中,说明发生了内存泄漏。

resourceType:检测对象的完全限定名称,主要用途用于报告内存泄漏时,相关的详细信息。

samplingInterval:内存泄漏检测级别是SIMPLE时,的检测频率,默认113,单位为个,而不是时间。

maxActive:该类对象的最大激活数量。

active        :当前激活的数量

loggedTooManyActive  :  active * samplingInterval > maxActive
条件满足时,是否打印日志。
4.2.1 ResourceLeakDetector open方法详解
AbstractByteBufAllocator 的toLeakAwareBuffer入口,然后进入到ResourceLeakDetector
open方法
protected static ByteBuf toLeakAwareBuffer(ByteBuf buf) {
ResourceLeak leak;
switch (ResourceLeakDetector.getLevel()) {
case SIMPLE:
leak = AbstractByteBuf.leakDetector.open(buf);
if (leak != null) {
buf = new SimpleLeakAwareByteBuf(buf, leak);
}
break;
case ADVANCED:
case PARANOID:
leak = AbstractByteBuf.leakDetector.open(buf);
if (leak != null) {
buf = new AdvancedLeakAwareByteBuf(buf, leak);
}
break;
}
return buf;
}
该方法的意思是如果返回一个ResoureLeak,则用ResourceLeak与当前的ByteBuf放入一个包装类,跟踪该直接内存的回收情况,检测内存泄露。如果没有产生一个ResourceLeak,则不会跟踪直接内存的泄露检测。

/**
* Creates a new {@link ResourceLeak} which is expected to be closed via {@link ResourceLeak#close()} when the
* related resource is deallocated.
*
* @return the {@link ResourceLeak} or {@code null}
*/
public ResourceLeak open(T obj) {
Level level = ResourceLeakDetector.level;
if (level == Level.DISABLED) {
return null;
}

if (level.ordinal() < Level.PARANOID.ordinal()) {
if (leakCheckCnt ++ % samplingInterval == 0) {
reportLeak(level);
return new DefaultResourceLeak(obj);
} else {
return null;
}
} else {
reportLeak(level);
return new DefaultResourceLeak(obj);
}
}


//如果使用检测级别是SIMPLE时,每产生113个直接内存对象时,才会跟踪一个。我个人觉得有问题,为什么不每次产生一个,都跟踪,但只每产生113个时,调用一次reportLeak方法检测内存泄露。
如果修改为,是否更加合理些呢?
public ResourceLeak open(T obj) {
Level level = ResourceLeakDetector.level;
if (level == Level.DISABLED) {
return null;
}

if (level.ordinal() < Level.PARANOID.ordinal()) {
if (leakCheckCnt ++ % samplingInterval == 0) {
reportLeak(level);
}
return new DefaultResourceLeak(obj);//这里会形成一条激活对象的引用链,该对象类似与双链表源码的Node,对象,就是一种双链表结构。
} else {
reportLeak(level);
return new DefaultResourceLeak(obj);
}
}

接下来,主要以reportLeak方法,检测内存泄露方法来说明整个实现机制。

private void reportLeak(Level level) {
if (!logger.isErrorEnabled()) {
for (;;) {
@SuppressWarnings("unchecked")
DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
if (ref == null) {
break;
}
ref.close();
}
return;
}

// Report too many instances.
int samplingInterval = level == Level.PARANOID? 1 : this.samplingInterval;
if (active * samplingInterval > maxActive && loggedTooManyActive.compareAndSet(false, true)) {
logger.error("LEAK: You are creating too many " + resourceType + " instances.  " +
resourceType + " is a shared resource that must be reused across the JVM," +
"so that only a few instances are created.");
}

// Detect and report previous leaks.
for (;;) {     //  @1
@SuppressWarnings("unchecked")
DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();    //@2
if (ref == null) {
break;
}

ref.clear();

if (!ref.close()) {   // @3
continue;
}

String records = ref.toString();
if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) {   //@3
if (records.isEmpty()) {
logger.error("LEAK: {}.release() was not called before it's garbage-collected. " +
"Enable advanced leak reporting to find out where the leak occurred. " +
"To enable advanced leak reporting, " +
"specify the JVM option '-D{}={}' or call {}.setLevel()",
resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));
} else {
logger.error(
"LEAK: {}.release() was not called before it's garbage-collected.{}",
resourceType, records);
}
}
}
}


代码@1,开始内存泄露检测
代码@2,从垃圾回收器通知的引用队列中找(位于该队列中的引用代表该引用执向的对象已经被垃圾回收器回收了),如果调用close方法,返回false,说明没有泄露,close方法第一次调用时,会返回true,将DefaultResourceLeak的
free设置为true,表示已经释放,所以在检测是否泄露的时候,只要内存泄露程序调用close不是第一次调用,就可以说明内存未泄露。
代码@3,reportedLeaks 存放的是发生内存泄露的信息。
Netty内存检测原理:
利用虚引用跟踪ByteBuf的对象的回收,并在垃圾回收后检测ByteBuf的release方法是否有执行过,其实就是DefaultResoureLeak的close方法是否执行过判断是否发生了内存泄漏。同时利用装饰模式,将用于跟踪垃圾回收的虚引用DefaultRersourceLeak与具体的BtyeBuf包装起来,,装饰类:WrappedByteBuf进行包装。

关于Netty的分析就到处为止,下一篇将学习Netty PooledByteBuf的实现原理,敬请期待。。。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: