7、Netty那些事 - 内存泄漏检测

359 阅读7分钟

莫道桑榆晚,为霞尚满天。居家隔离的第32天,中间曾经放开了一个礼拜,出去呼吸了下自由的空气,采购了一些物资,坚持了半个月,又到了“山穷水尽”的地步了,但是比起那些离开的人,自己始终幸运了许多。俗话说,大疫不过三年,我始终相信,我们会战胜疫情,请大家多一点耐心,少一点牢骚,共勉。

一、前言

今天是Netty源码系列的第七篇,也将开始一个新的知识点即《Netty内存相关源码解读》,想了很多切入点,比如从前文分析Netty读写流程继续入手,分析Netty读写流程再对op_read事件解读时,曾提到过Netty的内存分配,也就是Netty内存的入口,从这里切入其实更符合逻辑,不过想了想可能有些同学可能还看过,就找了个相对独立的内存管理的知识点《内存泄漏检测》来切入,也算是自己对学过的知识梳理。

二、引入“哼哈四将”

大家都是搞java的,由于JVM的封装,大家平时对内存的申请和释放关注的可能不多,C++大佬除外。Netty内部模式采用池化的PooledByteBuf来处理数据的读写,以提高程序性能。不过PooledByteBuf需要手动释放,否则会造成内存泄漏。对于Netty这么复杂的工程来说,排查内存泄漏可不是一件容易的事情,因此,Netty运用了JDK的弱引用和引用队列设计了一套专门的内存检测机制,主动发现内存泄漏问题,实现了ByteBuf对象的监控。

提取到弱引用,这里多提一句,java对于引用类型做了区分,一共四种

  • 强引用
    • 最常用的引用类型,比如new Object(),只要这个变量可用,垃圾回收器就不会主动回收。
  • 软引用
    • 当内存不足时,被垃圾回收器回收。
  • 弱引用
    • 垃圾回收器线程扫描它所管辖的内存区域的时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存,与引用队列配合使用,会将回收的对象放入队列中。
  • 虚引用
    • 任何时候可能都被回收

其实以上知识都是老生常谈了,想必学过java的同学可能多知道,不过,在实际生产中真正应用过的我想肯定不多,这也是学习源码的意义所在,通过学习优秀的设计,提高我们解决业务问题的能力。

三、内存泄漏检测原理

Netty的内存泄漏检测机制主要是检测ByteBuf的内存是否正常释放,要想实现这个功能,如果让你来设计怎么做呢?

image.png

原理类似《把大象装进冰箱拢共分几步》,首先既然要检测,那么就要记录当前有哪些ByteBuf对象吧(First)。

然后如果真的检测到发生泄漏,那么得打日志记录下吧,记录下当前ByteBuf被哪个类持有的,是怎么调用的,从而达到追溯的效果(Second)。

最后则需要进行内存泄漏的检测,检查“漏网之鱼”(Third)。

  • 信息采集

采集入口在内存分配器PooledByteBufAllocator的newDirectBuffer和newHeapBuffer方法中,两个方法都会调用AbstractByteBufAllocator类的toLeakAwareBuffer方法,返回被包装后的ByteBuf对象,源码如下:

    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;
    }

最终会返回两种包装类,即SimpleLeakAwareByteBuf和AdvancedLeakAwareByteBuf。二者的区别在于,前者只在ByteBuf被销毁时告知内存泄漏检测工具把正常的对象从检测缓存中移除,方便判断是否泄漏;后者则是前者的子类,主要作用是记录ByteBuf的调用轨迹,用来进行追溯

  • 调用栈记录 每个ByteBuf的最新调用栈信息记录在其弱引用中,这个弱引用和byteBuf对象都存在SimpleLeakAwareByteBuf类中,源码如下:
    private final ByteBuf trackedByteBuf;
    final ResourceLeakTracker<ByteBuf> leak;
  • 泄漏检测 在创建弱引用的时,需要引用队列的配合,当检测内存泄漏的时候,需要遍历引入队列,找到已经被回收的ByteBuf引用,通过这些引用判断是否调用了ByteBuf的销毁接口来检测是否存在泄漏。

这里有个点要注意下,弱引用如果没有和强引用关联,会再下一次被垃圾回收器回收,因此Netty采用全局Set把弱引用缓存起来,防止弱引用再遍历前被回收,因为如果提前回收了,byteBuf对象可能还没来得及被使用就被回收了,得不偿失。

四、ResourceLeakDetector源码解读

在分析SimpleLeakAwareByteBuf类时候,可以看到有一个属性类型为ResourceLeakTracker,默认实现为DefaultResourceLeak,为ResourceLeakDetector的内部类。因此ResourceLeakDetector在整个内存泄漏检测检测中有着核心作用。

  • 一种缓存区资源会创建一个ResourceLeakDetector实例,并监控次缓冲区类型的池化资源
  • 运用全局的引用队列和引用缓存Set构建ByteBuf的弱引用对象,检测当前资源是否出现了内存泄漏
  • 如果出现内存泄漏,则输出内存泄漏报告

4.1. 内存检测级别

Netty的内存泄漏检测机制有四种:

  • DISABLED
    • 禁用,不开启检测
  • SIMPLE
    • Netty的默认配置,按照一定的比例检测。检测到泄漏,会打印LEAK:XXXX等日志,但没有任何调用栈,因为使用的是SimpleLeakAwareByteBuf包装类
  • ADVANCED
    • 和SIMPLE一样,使用的是AdvancedLeakAwareByteBuf包装类,会输出调用栈信息
  • PARANOID
    • 在ADVANCED级别上按照100%比例采集

开发阶段可以设置为PARANOID,线上出现内存泄漏后可以调成ADVANCED观察调用栈,平时没必要开启,浪费资源。

4.2. trace方法

ResourceLeakDetectorh类的trace方法用来采集buf,感觉设置的采集级别进行内存泄漏检测,源码如下;

    public final ResourceLeakTracker<T> track(T obj) {
        return track0(obj);
    }

    private DefaultResourceLeak track0(T obj) {
        Level level = ResourceLeakDetector.level;
        // 不检测,不采集
        if (level == Level.DISABLED) {
            return null;
        }
        // 获取一个128以内的随机数,用来执行抽样检查
        if (level.ordinal() < Level.PARANOID.ordinal()) {
            // 为0则进行采集
            if ((PlatformDependent.threadLocalRandom().nextInt(samplingInterval)) == 0) {
                // 检测是否有泄漏
                reportLeak();
                return new DefaultResourceLeak(obj, refQueue, allLeaks, getInitialHint(resourceType));
            }
            return null;
        }
        // 全部采集
        reportLeak();
        // 会把创建的DefaultResourceLeak对象放入allLeaks全局引用队列
        return new DefaultResourceLeak(obj, refQueue, allLeaks, getInitialHint(resourceType));
    }

这里的的refQueue就是弱引用关联的队列;allLeaks则是全局的弱引用对象集合,相关定义如下:

public class ResourceLeakDetector<T> {
    ... ...
    private final ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
    private final Set<DefaultResourceLeak<?>> allLeaks =
            Collections.newSetFromMap(new ConcurrentHashMap<DefaultResourceLeak<?>, Boolean>());

}

reportLeak源码如下:

    private void reportLeak() {
        if (!needReport()) {
            clearRefQueue();
            return;
        }

        // 循环获取引用队列中的弱引用
        for (;;) {
            DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
            if (ref == null) {
                break;
            }
            
            // 检测是否泄泄漏
            if (!ref.dispose()) {
                continue;
            }
            
            // 获取buf的调用栈信息,这里重写了toString方法,遍历调用栈链表
            String records = ref.getReportAndClearRecords();
            // 已经输出泄漏信息就不重复输输出了
            if (reportedLeaks.add(records)) {
                if (records.isEmpty()) {
                    reportUntracedLeak(resourceType);
                } else {
                    // 输出内存泄漏日志和调用栈信息
                    reportTracedLeak(resourceType, records);
                }
            }
        }
    }
    protected void reportTracedLeak(String resourceType, String records) {
        // 对输出日志感兴趣的同学可以看下https://netty.io/wiki/reference-counted-objects.html这个链接
        logger.error(
                "LEAK: {}.release() was not called before it's garbage-collected. " +
                "See https://netty.io/wiki/reference-counted-objects.html for more information.{}",
                resourceType, records);
    }

4.3. 调用栈保存

在ADVANCED级别以上,ByteBuf的每项操作都会记录调用栈(在AdvancedLeakAwareByteBuf类中调用ResourceLeakDetector.record方法)记录调用栈时会 创建一个Record对象,该对象继承了Throwable,因此创建Record对象时,当前线程的调用栈就会被保存起来。调用栈保存源码如下:

    public void record() {
        record0(null);
    }
        private void record0(Object hint) {
            // TARGET_RECORDS >0 则记录,控制这调用链路的链表长度
            if (TARGET_RECORDS > 0) {
                TraceRecord oldHead;
                TraceRecord prevHead;
                TraceRecord newHead;
                boolean dropped;
                do {
                    // 链表头为空表示已经关闭
                    if ((prevHead = oldHead = headUpdater.get(this)) == null) {
                        // already closed.
                        return;
                    }
                    // 链表长度判断
                    final int numElements = oldHead.pos + 1;
                    if (numElements >= TARGET_RECORDS) {
                        final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30);
                        if (dropped = PlatformDependent.threadLocalRandom().nextInt(1 << backOffFactor) != 0) {
                            prevHead = oldHead.next;
                        }
                    } else {
                        dropped = false;
                    }
                    // 创建一个新的record,添加到链表上,头插法
                    newHead = hint != null ? new TraceRecord(prevHead, hint) : new TraceRecord(prevHead);
                } while (!headUpdater.compareAndSet(this, oldHead, newHead));
                if (dropped) {
                    droppedRecordsUpdater.incrementAndGet(this);
                }
            }
        }

五、小结

本文主要分析了Netty内存检测相关知识点,Netty内存检测功能是java弱引用设计的,内部会维护一个ReferenceQueue用来存放当前的弱引用对象,同时支持四种内存泄漏检测模式,由于弱引用对象在GC的时候会被回收,防止被提前回收,通过一个全局的强引用队列来存储所有的弱引用对象。当byteBuf对象被调用时,需要调用record方法,通过头插法维护当前的调用栈,用来做调用追溯。