Netty随笔集 -- ResourceLeakDetector源码解析

163 阅读5分钟

1、作用

用于内存申请后的内存对象检测泄露; 为什么会有这个?

  1. Netty为了防止数据之间的来回拷贝,提升数据传输效率,故默认使用堆外内存;(因为堆内内存的数据传递到堆外,也需要一次数据拷贝)
  2. Netty申请的堆外内存,需要额外注意堆外内存的主动管理(申请、重利用、释放等);如果申请没有释放,就可能会有内存泄露的风险;

2、使用方式

调用API PooledByteBufAllocator.newDirectBuffer 或者是PooledByteBufAllocator.newHeapBuffer 便可自动触发LeakDetector检测

protected static ByteBuf toLeakAwareBuffer(ByteBuf buf) {
    ResourceLeakTracker<ByteBuf> leak;
    switch (ResourceLeakDetector.getLevel()) {
        case SIMPLE:
            leak = AbstractByteBuf.leakDetector.track(buf);
            if (leak != null) {
                buf = new SimpleLeakAwareByteBuf(buf, leak);
            }
            break;
        case ADVANCED:
        case PARANOID:
            leak = AbstractByteBuf.leakDetector.track(buf);
            if (leak != null) {
                buf = new AdvancedLeakAwareByteBuf(buf, leak);
            }
            break;
        default:
            break;
    }
    return buf;
}

3、原理

一句话概括:在申请内存时,进行检测是否有内存引用的没有被释放,如果有,则说明内存有泄漏;

  1. 通过调用newDirectBufferAPI时,利用Delegate模式进行封装一层包装成LeakAwareByteBuf
  2. 当创建LeakAwareByteBuf对象时,做一次检测,将ResourceLeakDetector中的所有DefaultResourceLeak进行遍历,查看head == null; 如果为空,则已经close;如果不为空,则说明有内存泄露;
  3. 注意:每次创建对象的时候,才会做一次内存检测;

3.1 关键技术点-虚引用

可以参看我的另一篇文章 -- Java中弱引用在开源框架如何使用?

3.3 整体类图

classDiagram
AbstractByteBuf <|-- PooledDirectedByteBuf :继承
PooledDirectedByteBuf --> LeakAwareByteBuf
AbstractByteBuf --> ResourceLeakDetector
ResourceLeakDetector o-- DefaultResourceLeak
ResourceLeakTracker <|-- DefaultResourceLeak
WeakReference<|-- DefaultResourceLeak

class AbstractByteBuf {
+ResourceLeakDetector detector  
}

class ResourceLeakDetector{
-Set<DefaultResourceLeak> allLeaks
-ReferenceQueue refQueues
-ResourceLeak level
+reportLeak()
-track0()
}
class DefaultResourceLeak{
-Record head
-AtomicReferenceFieldUpdater headUpdater
-Set<DefaultLeakResource> allLeaks
}
class ResourceLeakTracker{
+record()
}

3.4 整体流程图

image.png

4、源码解析

4.1 如何检测内存泄露

入口:toLeakAwareBuffer

检测级别

  1. SIMPLE : 简单信息,而且是抽样获取使用该内存对象的位置;
  2. ADVANCED:提供更详细的上下文信息,跟踪资源的整个生命周期轨迹;
  3. PARANOID:仅限调试使用,该方式会对资源的每一种使用方式都会进行跟踪、记录,内容非常详细;但也是最耗时
protected static ByteBuf toLeakAwareBuffer(ByteBuf buf) {
    ResourceLeakTracker<ByteBuf> leak;
    // 根据检测级别进行判断封装哪一种Delegator
    switch (ResourceLeakDetector.getLevel()) {
        case SIMPLE:
        // 检测buffer使用
            leak = AbstractByteBuf.leakDetector.track(buf);
            if (leak != null) {
                buf = new SimpleLeakAwareByteBuf(buf, leak);
            }
            break;
        case ADVANCED:
        case PARANOID:
            leak = AbstractByteBuf.leakDetector.track(buf);
            if (leak != null) {
                buf = new AdvancedLeakAwareByteBuf(buf, leak);
            }
            break;
        default:
            break;
    }
    return buf;
}

采样跟踪 LeakResourceDetector.track

private DefaultResourceLeak track0(T obj) {
    Level level = ResourceLeakDetector.level;
    if (level == Level.DISABLED) {
        return null;
    }
// 如果检测等级小于PARANOID,就会随机采样进行定期检测;
    if (level.ordinal() < Level.PARANOID.ordinal()) {
    // 默认samplingInterval = 128, 也就是1/128的概率进行检测;
        if ((PlatformDependent.threadLocalRandom().nextInt(samplingInterval)) == 0) {
            reportLeak();
            return new DefaultResourceLeak(obj, refQueue, allLeaks);
        }
        return null;
    }
    // 如果检测等级=PARANOID, 直接检测;
    reportLeak();
    return new DefaultResourceLeak(obj, refQueue, allLeaks);
}

弱引用判断 LeakResourceDetector.reportLeak

private void reportLeak() {
// 如果没有开启error级别,就不会打印;基本走不进去这个判断;
    if (!logger.isErrorEnabled()) {
        clearRefQueue();
        return;
    }

    // Detect and report previous leaks.
    for (;;) {
        @SuppressWarnings("unchecked")
        // 从弱引用队列里面获取元素,一旦有元素,说明这个对象已经被GC掉,这里做一个检测用于后置清理;
        // 这种黑科技方式使用的还比较多,普遍用于资源的后置清理
        DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
        if (ref == null) {
            break;
        }

        if (!ref.dispose()) {
            continue;
        }
        // 调用DefaultResourceLeak.toString()
        String records = ref.toString();
        if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) {
            if (records.isEmpty()) {
                reportUntracedLeak(resourceType);
            } else {
                reportTracedLeak(resourceType, records);
            }
        }
    }
}

输出所有调用点堆栈:DefaultResourceLeak.toString

public String toString() {
    Record oldHead = headUpdater.getAndSet(this, null);
    // 如果headUpdater指向的元素是null,则说明已经关闭了;直接返回即可;
    if (oldHead == null) {
        // Already closed
        return EMPTY_STRING;
    }

    final int dropped = droppedRecordsUpdater.get(this);
    int duped = 0;
    int present = oldHead.pos + 1;
    // Guess about 2 kilobytes per stack trace
    StringBuilder buf = new StringBuilder(present * 2048).append(NEWLINE);
    buf.append("Recent access records: ").append(NEWLINE);

    int i = 1;
    Set<String> seen = new HashSet<String>(present);
    // 遍历当前链表,如果是链尾,就把Created at字符拼上,说明是创建的地方;
    // 如果不是链尾,则正常将序号拼上,说明有多少处使用的地方;
    // 这里也涉及到Record.toString 
    for (; oldHead != Record.BOTTOM; oldHead = oldHead.next) {
        String s = oldHead.toString();
        if (seen.add(s)) {
            if (oldHead.next == Record.BOTTOM) {
                buf.append("Created at:").append(NEWLINE).append(s);
            } else {
                buf.append('#').append(i++).append(':').append(NEWLINE).append(s);
            }
        } else {
            duped++;
        }
    }

    if (duped > 0) {
        buf.append(": ")
                .append(duped)
                .append(" leak records were discarded because they were duplicates")
                .append(NEWLINE);
    }

    if (dropped > 0) {
        buf.append(": ")
           .append(dropped)
           .append(" leak records were discarded because the leak record count is targeted to ")
           .append(TARGET_RECORDS)
           .append(". Use system property ")
           .append(PROP_TARGET_RECORDS)
           .append(" to increase the limit.")
           .append(NEWLINE);
    }

    buf.setLength(buf.length() - NEWLINE.length());
    return buf.toString();
}

打印详细堆栈:Record.toString

实现很简单;Record继承Throwable,直接调用getStackTrace 方法就可以获取当前的堆栈信息; 这里就不贴代码了;

4.2 如何记录内存泄漏位置

以AdvancedLeakAwareBytebuf为例,以 readByte为例; AdvancedLeakAwareBytebuf就是一个Delegator,用于添加、丰富额外逻辑

入口 AdvancedLeakAwareBytebuf.readByte()

public byte readByte() {
    recordLeakNonRefCountingOperation(leak);
    return super.readByte();
}

内存记录:AdvancedLeakAwareByteBuf.recordLeakNonRefCountingOperation

static void recordLeakNonRefCountingOperation(ResourceLeakTracker<ByteBuf> leak) {
    if (!ACQUIRE_AND_RELEASE_ONLY) {
        leak.record();
    }
}

DefaultResourceLeak.record

private void record0(Object hint) {
    // Check TARGET_RECORDS > 0 here to avoid similar check before remove from and add to lastRecords
    if (TARGET_RECORDS > 0) {
        Record oldHead;
        Record prevHead;
        Record newHead;
        boolean dropped;
        do {
        // 如果已经null,说明已经关闭了;
            if ((prevHead = oldHead = headUpdater.get(this)) == null) {
                // already closed.
                return;
            }
            final int numElements = oldHead.pos + 1;
            // TARGET_RECORDS 默认是4;如果当前添加的Record>=4,就会采取抽样进行丢弃;
            // 因为采样太多,就会导致占用内存过多;浪费空间;
            if (numElements >= TARGET_RECORDS) {
            // 举例:若numElements=8, 就会从(0,1<<4)区间内选择一个数字,判断是否=0,若等于0,则说明需要丢弃;丢弃的信息会在打印内存泄露的信息里面显示出来的;
                final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30);
                if (dropped = PlatformDependent.threadLocalRandom().nextInt(1 << backOffFactor) != 0) {
                    prevHead = oldHead.next;
                }
            } else {
                dropped = false;
            }
            newHead = hint != null ? new Record(prevHead, hint) : new Record(prevHead);
        } while (!headUpdater.compareAndSet(this, oldHead, newHead));
        if (dropped) {
            droppedRecordsUpdater.incrementAndGet(this);
        }
    }
}

内存释放 ByteBuf.release

public boolean release() {
    if (super.release()) {
    // 把对应的DefaultResourceLeak关闭掉;如何关闭?见下文
        closeLeak();
        return true;
    }
    return false;
}

DefaultResourceLeak.close 将headUpdater的head设置为null,代表已经释放,前文中在track的时候,就是根据head来进行判断;

public boolean close() {
    if (allLeaks.remove(this)) {
        // Call clear so the reference is not even enqueued.
        clear();
        headUpdater.set(this, null);
        return true;
    }
    return false;
}

5、学习心得

学有所获,方能不负韶华

5.1 什么时间点进行内存检测?

1、定时监测? 显然太low
2、在GC以后,垃圾回收的时候进行检测;可是这个时间点,垃圾对象已经被回收掉了,怎么才能判断呢?
3、弱、软、虚引用就排上用场了;
这里面DefaultResourceLeak就是一个弱引用,在里面维护了一个内存泄漏点链表(每个泄漏点都有描述)

image.png

  1. 当强引用对象被GC后,相对应的ByteBuf对象也会被销毁掉,但是这个ByteBuf对象所引用的堆外内存因为没有release释放,所以这个时候就会出现内存泄露,故DefaultResourceLeak的检测机制就出现了,每次GC后,在refQueue中就会出现对应的DefaultResourceLeak,然后判断其headUpdater是否为null即可,很精巧;

5.2 委托模式

利用LeakAwakeByteBuf做一层包装,来增加额外、附加的功能;

6 参考网址

内存泄露检测
Netty 如何自动探测内存泄露的发生