Netty详解之十一:ByteBuf内存泄露

我们现在知道ByteBuf是通过引用计数来管理生命周期的,换句话说,需要开发者手动管理,这对java程序员来说是非常有挑战性的一件事;为此,Netty提供了内存泄露检测机制。

ByteBuf泄露检测原理

首先ByteBuf是一个java对象,Netty并不关注java对象的泄露,使用者作为java开发者必须保证没有发生java对象泄露,在这个前提下,Netty为ByteBuf包含的数据区域的泄露提供诊断。

java对象泄露,是指意外地缓存了不再使用对象的强引用,更多相关知识请自行搜索。

假设开发者分配了一个ByteBuf,使用完之后,忘记调用ByteBuf.release,那么就发生泄露了;那么Netty怎么检测到这一事实呢?其实反而是依靠Java GC:假设ByteBuf这个java对象本身没有泄露,那么它最终会被java GC回收掉,如果一个ByteBuf对象被GC回收时,引用计数不为0,说明发生了泄漏

Netty为了跟踪ByteBuf的使用轨迹(尤其是引用计数的变化),提供了一个特殊的包装器类LeakAwareByteBuf;为了检测ByteBuf被gc回收这个事件,则使用了“java弱引用”技术。

LeakAwareByteBuf

LeakAwareByteBuf是一种Wrapped ByteBuf,它有两个实现类SimpleLeakAwareByteBuf和AdvancedLeakAwareByteBuf,分别实现简单的和高级的泄露检测功能。

SimpleLeakAwareByteBuf

class SimpleLeakAwareByteBuf extends WrappedByteBuf {

	 //要追踪的ByteBuf
	 private final ByteBuf trackedByteBuf;
	 
	 //追踪信息的记录者
    final ResourceLeakTracker<ByteBuf> leak;

    //要追踪的ByteBuf和要包装的ByteBuf可以不是一个对象(主要发生在CompositeByteBuf场景下)
  	 //暂时可忽略这一点,认为wrapped=trackedByteBuf即可
    SimpleLeakAwareByteBuf(ByteBuf wrapped, ByteBuf trackedByteBuf, ResourceLeakTracker<ByteBuf> leak) {
        super(wrapped);
        this.trackedByteBuf = ObjectUtil.checkNotNull(trackedByteBuf, "trackedByteBuf");
        this.leak = ObjectUtil.checkNotNull(leak, "leak");
    }

    SimpleLeakAwareByteBuf(ByteBuf wrapped, ResourceLeakTracker<ByteBuf> leak) {
        this(wrapped, wrapped, leak);
    }
    
    ...
    
    
    //slice&duplicate等接口实现,同样返回一个具备泄露检测功能的副本
    @Override
    public ByteBuf slice() {
        return newSharedLeakAwareByteBuf(super.slice());
    }
    
    ...
    
    
    //touch接口的语义是记录ByteBuf的使用轨迹,但SimpleLeakAwareByteBuf内是一个空实现
    @Override
    public ByteBuf touch() {
        return this;
    }

    @Override
    public ByteBuf touch(Object hint) {
        return this;
    }
    
    ...
    
    
    //如果ByteBuf被顺利释放了,停止追踪它
    @Override
    public boolean release() {
        if (super.release()) {
            closeLeak();
            return true;
        }
        return false;
    }

    private void closeLeak() {
        boolean closed = leak.close(trackedByteBuf);
        assert closed;
    }
}

SimpleLeakAwareByteBuf通过一个叫做ResourceLeakTracker的对象来追踪ByteBuf的轨迹,不过它的touch方法是空的,所以SimpleLeakAwareByteBuf只可以追踪ByteBuf的创建和释放,并不能追踪使用过程。

AdvancedLeakAwareByteBuf

AdvancedLeakAwareByteBuf扩展了SimpleLeakAwareByteBuf的功能。

final class AdvancedLeakAwareByteBuf extends SimpleLeakAwareByteBuf {


    static void recordLeakNonRefCountingOperation(ResourceLeakTracker<ByteBuf> leak) {
		leak.record();
    }
    
   
    ...
    
    
    //slice这样的方法,自动记录了操作轨迹
    @Override
    public ByteBuf slice(int index, int length) {
        recordLeakNonRefCountingOperation(leak);
        return super.slice(index, length);
    }
    
    ...
    
    //基本的访问接口,自动记录了轨迹
    @Override
    public byte getByte(int index) {
        recordLeakNonRefCountingOperation(leak);
        return super.getByte(index);
    }
    
    ...
    
    
    //touch,让使用者可以主动记录轨迹
    @Override
    public ByteBuf touch() {
        leak.record();
        return this;
    }

    @Override
    public ByteBuf touch(Object hint) {
        leak.record(hint);
        return this;
    }
    
}

从以上代码可以看出,所谓“高级”检测,就是更详细地记录了ByteBuf的使用轨迹。它们都是通过ResourceLeakTracker记录信息,这个类后面会介绍。

检测级别

Netty为内存泄露定义了几种级别(ResourceLeakDetector.Level):

  • DISABLED,不检测
  • SIMPLE,简单采样检测,使用SimpleLeakAwareByteBuf
  • ADVANCED,高级采样检测,使用AdvancedLeakAwareByteBuf
  • PARANOID,偏执检测,全量使用AdvancedLeakAwareByteBuf

采样是指,对新创建的ByteBuf有一定几率执行内存泄露检测。

内存泄露检测对性能有明显影响,一般,线上最多采用SIMPLE级别,推荐DISABLED;测试环境可以使用ADVANCED,调试时才开启PARANOID级别。

buf allocator

现在看buf allocator是如何使用LeakAwareByteBuf的,以UnpooledByteBufAllocator为例:

public class UnpooledByteBufAllocator {

	 //Unpooled Heap buffer没有使用内存泄露检测
	 @Override
    protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
        return PlatformDependent.hasUnsafe() ?
                new InstrumentedUnpooledUnsafeHeapByteBuf(this, initialCapacity, maxCapacity) :
                new InstrumentedUnpooledHeapByteBuf(this, initialCapacity, maxCapacity);
    }

	 //Unpooled direct buffer通过toLeakAwareBuffer包装原始ByteBuf
    @Override
    protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
        final ByteBuf buf;
        if (PlatformDependent.hasUnsafe()) {
            buf = noCleaner ? new InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf(this, initialCapacity, maxCapacity) :
                    new InstrumentedUnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity);
        } else {
            buf = new InstrumentedUnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
        }
        return disableLeakDetector ? buf : toLeakAwareBuffer(buf);
    }
}

//toLeakAwareBuffer是定义在基类的一个工厂方法,将原始Bytebuf对象包装为具备内存泄露检测功能的LeakAwareByteBuf
public class AbstractByteBufAllocator {
	 protected static ByteBuf toLeakAwareBuffer(ByteBuf buf) {
        ResourceLeakTracker<ByteBuf> leak;
        switch (ResourceLeakDetector.getLevel()) {
        	  //检测级别是Simple,
            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;
    }
}

ResourceLeakDetector.getLevel()是静态方法,说明内存泄露检测级别,是一个全局的设置。toLeakAwareBuffer依据level将buf包装为SimpleLeakAwareByteBuf或AdvancedLeakAwareByteBuf。而AbstractByteBuf.leakDetector.track方法创建ResourceLeakTracker对象来追踪buf,track方法实现了概率采样逻辑,在概率没有命中时,返回null。

ResourceLeakTracker

现在移步ResourceLeakTracker,看看它如何追踪ByteBuf是如何实现的。

ResourceLeakTracker的接口抽象如下;

public interface ResourceLeakTracker<T>  {

    //记录ByteBuf的使用轨迹
    void record();
    
    //记录轨迹,可以附加一个用户自定义提示信息
    void record(Object hint);
    
    //停止追踪ByteBuf
    boolean close(T trackedObject);
}

它的实现是ResourceLeakDetector的一个内部类,主要代码如下:

//ResourceLeakDetector可认为以单例形式存在
public class ResourceLeakDetector {
		
		//全局Leak对象集合
    	private final Set<DefaultResourceLeak<?>> allLeaks =
            Collections.newSetFromMap(new ConcurrentHashMap<DefaultResourceLeak<?>, Boolean>());

		//弱引用队列
    	private final ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
		
		//注意关键点,它是WeakReference子类,以弱引用方式追踪目标ByteBuf对象
		private staic class DefaultResourceLeak<T>
            extends WeakReference<Object> implements ResourceLeakTracker<T>, ResourceLeak {

		  //ByteBuf的每一个使用轨迹,是一个Record,多个Record形成一个链表
        private volatile Record head;
        
        //Record链表长度不可能无限制增长,在到达上限时,抛弃部分Record,这里记录抛弃的总数量
        private volatile int droppedRecords;

		 //这是一个反向引用,指向ResourceLeakDetector.allLeaks,可以认为是进程内所有DefaultResourceLeak的集合
        private final Set<DefaultResourceLeak<?>> allLeaks;
        
        //对象的hash值
        private final int trackedHash;

		  //referent指向目标ByteBuf
		  //refQueue是全局弱引用队列(这部分知识请自行查阅)
		  //allLeaks是全局的leak集合
        DefaultResourceLeak(
                Object referent,
                ReferenceQueue<Object> refQueue,
                Set<DefaultResourceLeak<?>> allLeaks) {
            super(referent, refQueue);

            trackedHash = System.identityHashCode(referent);
            allLeaks.add(this);
            this.allLeaks = allLeaks;
        }

        @Override
        public void record() {
            record0(null);
        }

        @Override
        public void record(Object hint) {
            record0(hint);
        }

        private void record0(Object hint) {
			  //这里创建Record节点,加入到链表
			  //我们对链表是如何维护的没有兴趣,关键是Record到底记录了什么
        }

		 //这是判定是否发生内存泄露发生的方法,如果this仍然在allLeaks集合中,那么说明发生内存泄露
		 //该方法和close方法配合
        boolean dispose() {
            clear();
            return allLeaks.remove(this);
        }

		 //LeakAwareByteBuf在引用计数为零时,调用close,说明ByteBuf成功释放,没有内存泄露
        @Override
        public boolean close() {
            if (allLeaks.remove(this)) {
                clear();
                headUpdater.set(this, null);
                return true;
            }
            return false;
        }
	}

	 //这里最关键的一点:Recod继承自Throwable,可以记录当前的调用栈
    private static final class Record extends Throwable {
        Record(Record next, Object hint) {
            // This needs to be generated even if toString() is never called as it may change later on.
            hintString = hint instanceof ResourceLeakHint ? ((ResourceLeakHint) hint).toHintString() : hint.toString();
            this.next = next;
            this.pos = next.pos + 1;
        }
    }
}

报告内存泄露

最后一个问题:内存泄露是怎么报告的,实现还是在ResourceLeakDetector里面。

public class ResourceLeakDetector {

	 //每次创建DefaultResourceLeak对象时,调用reportLeak来报告捕获的内存泄露
    private DefaultResourceLeak track0(T obj) {
        if (level.ordinal() < Level.PARANOID.ordinal()) {
            if ((PlatformDependent.threadLocalRandom().nextInt(samplingInterval)) == 0) {
                reportLeak();
                return new DefaultResourceLeak(obj, refQueue, allLeaks);
            }
            return null;
        }
        reportLeak();
        return new DefaultResourceLeak(obj, refQueue, allLeaks);
    }
    
    
    private void reportLeak() {
    	  //遍历弱引用队列
        for (;;) {
            DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
            if (ref == null) {
                break;
            }

			  //对于弱引用队列里的ref,它关联的ByteBuf对象被gc掉了
			  //dispose方法查看这个ref是否仍然全局allLeaks里面,如果是,说明发生了内存泄露;
			  //参照DefaultResourceLeak.close方法,buf的引用计数变零时,会从allLeaks移除
            if (!ref.dispose()) {
                continue;
            }
			
			  //打印泄露日志
            String records = ref.toString();
            if (reportedLeaks.add(records)) {
                if (records.isEmpty()) {
                    reportUntracedLeak(resourceType);
                } else {
                    reportTracedLeak(resourceType, records);
                }
            }
        }
    }
}

以上代码可以看出,在追踪一个新的Bytebuf时,检查并报告目前是否发生了泄露。

什么类型的ByteBuf能检测泄露

上面已经分析了Netty ByteBuf泄露检测的原理,底层是基于java GC和弱引用技术,实现层面通过将ByteBuf包装为LeakAwareByteBuf,来追踪它的使用轨迹。

这个技术原理决定了不是所有的ByteBuf类型都能被检测泄露:

  • heap ByteBuf不行(或者说没有意义),因为heap ByteBuf的数据域是一个java byte数组,它的生命周期和ByteBuf完全一样,由GC完全负责;
  • pooled byteBuf不行,因为pooled ByteBuf的回收不是通过gc,而是重新回到ByteBuf pool,进而无法通过gc来追踪是否发生了泄露;

实际上,只有unpooled direct ByteBuf适用这个检测机制,在项目测试阶段,为了检测是否有ByteBuf泄露,可以强制全部使用UnpooledByteBufAllocator(设置ChannelOption.ALLOCATOR=UnpooledByteBufAllocator.DEFAULT)(大部分平台默认开启direct内存模式)。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值