14-ReferenceCounted

ReferenceCounted

  • Netty 底层通过引用计数(ReferenceCounted)来对 ByteBuf 进行管理,使用引用计数管理可以在ByteBuf不被使用时回收到资源池,相比垃圾回收而言实时性更好。
  • ReferenceCounted 是引用计数的顶层接口。前面一篇文章讲到了Netty中的 ByteBuf,它用于存储数据,也是 Netty 中所有不同类型 ByteBuf 的顶层父类,而 ByteBuf就实现了 ReferenceCounted 接口,因此 ByteBuf 会实现(可能在其子类实现) ReferenceCounted 接口中定义好的引用计数管理功能。
public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> {

    ......
}
  • Netty为什么要使用引用计数管理ByteBuf,而不是直接用JVM的GC机制?这里有两个参考点,参考了引用文章[4]
1:netty为了实现zero copy使用了Direct Buffer,该buffer从Native Memory分配出来,分配和回收效率要远低于在Java Heap上
的对象,所以一般full gc来控制的,直接内存会自己检测情况而调用system.gc(),通过使用引用计数可以自己来控制该buffer的
释放。具体看PlatformDependent.freeDirectBuffer(buffer)的实现。

2:netty使用了Pooled的功能。有点像c++的对象池,先分配一大块内存,然后从里面切出一块一块的供你使用,引用计数为0放
入pool中,供后面使用。如果不按按引用计数规则使用这两种对象,将会导致内存泄露。所以测试的时候需要测试下是有有内存泄漏。

一、接口定义

1.1 注释

  • 下面是源码中关于 ReferenceCounted 接口的注释
/**
 * netty引用计数接口
 * <p>
 * <p>
 * 一个引用计数对象要求显示的 deallocation (可以理解为显示的释放内存,allocate的反义词)
 * A reference-counted object that requires explicit deallocation.
 * <p>
 * ReferenceCounted初始化的时候,引用计数是1,
 * 调用retain()方法引用计数加一,调用release()计数减一
 * 如果引用计数减少到{@code 0},则将显式释放对象,访问该释放了的对象通常会导致访问非法。
 * When a new {@link ReferenceCounted} is instantiated, it starts with the reference count of {@code 1}.
 * {@link #retain()} increases the reference count, and {@link #release()} decreases the reference count.
 * If the reference count is decreased to {@code 0}, the object will be deallocated explicitly, and accessing
 * the deallocated object will usually result in an access violation.
 * </p>
 * <p>
 * 如果实现了 ReferenceCounted 接口的是一个容器对象,并且这个对象包含另一个也实现了 ReferenceCounted 接口的对象
 * 那么当容器对象的引用计数变为0的时候,内部被包含的对象也会被释放
 * If an object that implements {@link ReferenceCounted} is a container of other objects that implement
 * {@link ReferenceCounted}, the contained objects will also be released via {@link #release()} when the container's
 * reference count becomes 0.
 * </p>
 */

1.2 方法

  • 主要方法:主要是对引用计数的增减操作
public interface ReferenceCounted {

    /**
     * 获得对象的引用计数,如果是0以为这对象会被释放
     * <p>
     * Returns the reference count of this object.  If {@code 0}, it means this object has been deallocated.
     */
    int refCnt();

    /**
     * 引用计数加1
     * <p>
     * Increases the reference count by {@code 1}.
     */
    ReferenceCounted retain();

    /**
     * 引用计数加 n
     * <p>
     * Increases the reference count by the specified {@code increment}.
     */
    ReferenceCounted retain(int increment);
    
    /**
     * 引用计数减1
     * 当引用计数为 0 时,释放,当且仅当引用计数为0时,对象会被释放
     * <p>
     * Decreases the reference count by {@code 1} and deallocates this object if the reference count reaches at
     * {@code 0}.
     *
     * @return {@code true} if and only if the reference count became {@code 0} and this object has been deallocated
     */
    boolean release();

    /**
     * 引用计数减n
     * 当引用计数为 0 时,释放,当且仅当引用计数为0时,对象会被释放
     * <p>
     * Decreases the reference count by the specified {@code decrement} and deallocates this object if the reference
     * count reaches at {@code 0}.
     *
     * @return {@code true} if and only if the reference count became {@code 0} and this object has been deallocated
     */
    boolean release(int decrement);
}
  • 其他方法:调试相关方法
public interface ReferenceCounted {
 
    /**
     * 等价于调用 `#touch(null)` 方法,即 hint 方法参数传递为 null 。
     * <p>
     * Records the current access location of this object for debugging purposes.
     * If this object is determined to be leaked, the information recorded by this operation will be provided to you
     * via {@link ResourceLeakDetector}.  This method is a shortcut to {@link #touch(Object) touch(null)}.
     */
    ReferenceCounted touch();

    /**
     * 出于调试目的,用一个额外的任意的(arbitrary)信息记录这个对象的当前访问地址.
     * 如果这个对象被检测到泄露了, 这个操作记录的信息将通过ResourceLeakDetector 提供.
     * <p>
     * <p>
     * Records the current access location of this object with an additional arbitrary information for debugging
     * purposes.  If this object is determined to be leaked, the information recorded by this operation will be
     * provided to you via {@link ResourceLeakDetector}.
     */
    ReferenceCounted touch(Object hint);

}

二、ReferenceCounted的实现

2.1 ByteBuf

  • ByteBuf 中对 ReferenceCounted 的实现: ByteBuf 是一个抽象类,虽然实现了 ReferenceCounted 接口但是对很多方法并没有实现,对实现的方法也是定义成 abstract ,交给子类去实现,比如下面四个方法:
    @Override
    public abstract ByteBuf retain(int increment);

    @Override
    public abstract ByteBuf retain();

    @Override
    public abstract ByteBuf touch();

    @Override
    public abstract ByteBuf touch(Object hint);

2.2 AbstractReferenceCountedByteBuf

  • ReferenceCounted 的方法几乎都在 AbstractReferenceCountedByteBuf 中实现 ,下面是继承图,从图中看到 ByteBuf 抽象类实现了 ReferenceCounted 接口,ReferenceCounted 接口的方法在 AbstractReferenceCountedByteBuf 抽象类中得到实现,几乎全部的子类都会从 AbstractReferenceCountedByteBuf 继承这些方法,能够使用 ReferenceCounted 的引用计数机制来管理,这种设计方式在源码中经常可见。

在这里插入图片描述

  • AbstractReferenceCountedByteBuf 源码如下,一些读取方法省略了:
/**
 * ByteBuf 的用于实现引用计数的抽象类
 * Abstract base class for {@link ByteBuf} implementations that count references.
 */
public abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf {

    /**
     * {@link #refCnt} 一样计数的更新器,原子更新对象的引用计数成员变量,
     * 所有的ByteBuf实现类都继承自AbstractReferenceCountedByteBuf,因此对于所有的实例而言引用计数成员变量都是
     * AbstractReferenceCountedByteBuf类的 refCnt 字段
     */
    private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater = AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt");

    /**
     * 引用计数成员变量,volatile保证可见性
     */
    private volatile int refCnt;

    /**
     * 一个ByteBuf对象一旦被创建,引用计数就是1
     */
    protected AbstractReferenceCountedByteBuf(int maxCapacity) {
        // 设置最大容量
        super(maxCapacity);
        // 初始 refCnt 为 1
        refCntUpdater.set(this, 1);
    }

    /**
     * 直接修改 refCnt
     * An unsafe operation intended for use by a subclass that sets the reference count of the buffer directly
     */
    protected final void setRefCnt(int refCnt) {
        refCntUpdater.set(this, refCnt);
    }

    /**
     * 引用计数自增
     */
    @Override
    public ByteBuf retain() {
        return retain0(1);
    }

    /**
     * 引用计数增加 increment,校验 increment 为正数
     */
    @Override
    public ByteBuf retain(int increment) {
        return retain0(checkPositive(increment, "increment"));
    }

    /**
     * 引用计数增加的实现方法
     */
    private ByteBuf retain0(final int increment) {
        //1.由更新器来实现增加,方法返回的是旧值
        int oldRef = refCntUpdater.getAndAdd(this, increment);
        //2.如果 旧值<=0,或者 increment<=0,那么就还原旧值,并且抛出异常
        if (oldRef <= 0 || oldRef + increment < oldRef) {
            // Ensure we don't resurrect (which means the refCnt was 0) and also that we encountered an overflow.
            // 加回去,负负得正。
            refCntUpdater.getAndAdd(this, -increment);
            // 抛出 IllegalReferenceCountException 异常
            throw new IllegalReferenceCountException(oldRef, increment);
        }
        return this;
    }

    @Override
    public ByteBuf touch() {
        return this;
    }

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

    /**
     * 引用计数释放
     */
    @Override
    public boolean release() {
        return release0(1);
    }

    /**
     * 引用计数释放 decrement
     */
    @Override
    public boolean release(int decrement) {
        return release0(checkPositive(decrement, "decrement"));
    }

    /**
     * 引用计数释放的实现方法
     */
    @SuppressWarnings("Duplicates")
    private boolean release0(int decrement) {
        //1.减少
        int oldRef = refCntUpdater.getAndAdd(this, -decrement);
        //2.oldRef等于减少的值,说明减去后为0,需要释放
        if (oldRef == decrement) {
            // 释放
            deallocate();
            return true;
            //3.减少的值得大于原有oldRef会导致减去后为负数,不允许,或者decrement为负数,也不允许
        } else if (oldRef < decrement || oldRef - decrement > oldRef) {
            // Ensure we don't over-release, and avoid underflow.
            // 加回去,负负得正。
            refCntUpdater.getAndAdd(this, decrement);
            // 抛出 IllegalReferenceCountException 异常
            throw new IllegalReferenceCountException(oldRef, -decrement);
        }
        return false;
    }

    /**
     * Called once {@link #refCnt()} is equals 0.
     * 当引用计数减少为0时,调用
     */
    protected abstract void deallocate();
}
  • 代码不复杂,都是围绕着内部的引用计数变量 refCnt 的操作,还是比较简单清晰的,主要是用到了 AtomicIntegerFieldUpdater 来实现字段原子更新
2.2.1 AtomicIntegerFieldUpdater
  • AtomicIntegerFieldUpdater 可以原子的更新一个类的对象的指定字段,对应的类和字段名称通过构造方法指定,AtomicIntegerFieldUpdater是JUC包下的工具类,和 AtomicInteger 等原子变量在一个包下,底层使用的是CAS自旋锁机制,关于自旋锁部分可以阅读参考文章[1]
2.2.2 deallocate方法
  • 当一个 ByteBuf 对象的引用计数减少为0时,该对应即不能被访问,同时需要释放该对象,AbstractReferenceCountedByteBuf 中定义了 deallocate 方法释放对象方法但是并未实现,因为对于不同类型的 ByteBuf其实现方式有所不同
  /**
     * Called once {@link #refCnt()} is equals 0.
     * 当引用计数减少为0时,调用
     */
    protected abstract void deallocate();

三、deallocate的实现

  • 看看不同类型ByteBuf下,deallocate方法的实现

3.1 PooledByteBuf

  • PooledByteBuf的deallocate实现:前面PooledByteBuf的四个实现类,都是使用 PooledByteBuf的deallocate实现
    @Override
    protected final void deallocate() {
        //handle代表内存块所处位置
        if (handle >= 0) {
            // 属性重置
            final long handle = this.handle;
            this.handle = -1;
            memory = null;
            tmpNioBuf = null;
            // 释放内存回 Arena 中
            chunk.arena.free(chunk, handle, maxLength, cache);
            chunk = null;
            // 回收对象
            recycle();
        }
    }
  • PooledByteBuf会使用一个内存缓冲池,因此会对内存块进行回收以及相关引用属性的重置

3.2 UnpooledHeapByteBuf

  • UnpooledHeapByteBuf 是非池化的堆上分配ByteBuf,实现如下:
    @Override
    protected void deallocate() {
        // 释放老数组 ,array 为内部的字节数组
        freeArray(array);
        // 设置为空字节数组
        array = EmptyArrays.EMPTY_BYTES;
    }
  • freeArray方法由子类实现,我们看一个 UnpooledHeapByteBuf 的实现类 InstrumentedUnpooledHeapByteBuf 的实现,该类是内存分配器UnpooledByteBufAllocator的内部类,其内部实现是通过内存分配器来释放内存的
    private static final class InstrumentedUnpooledHeapByteBuf extends UnpooledHeapByteBuf {
        ...
        
        @Override
        protected void freeArray(byte[] array) {
            int length = array.length;
            super.freeArray(array);
            // Metric
            ((UnpooledByteBufAllocator) alloc()).decrementHeap(length);
        }
    }

3.3 UnpooledDirectByteBuf

  • UnpooledDirectByteBuf 是非池化的直接内存上分配ByteBuf,实现如下:
    @Override
    protected void deallocate() {
        ByteBuffer buffer = this.buffer;
        //buffer是直接内存的引用对象,将其置为null即可
        //,如果已经是null则返回
        if (buffer == null) {
            return;
        }
        //将buffer置null
        this.buffer = null;

        // 释放 buffer 对象
        if (!doNotFree) {
            freeDirect(buffer);
        }
    }
  • freeDirect 方法:通过平台依赖类来释放直接内存
    protected void freeDirect(ByteBuffer buffer) {
        PlatformDependent.freeDirectBuffer(buffer);
    }

四、ReferenceCounted操作

  • 前面是关于引用计数介绍,在具体的使用过程中,为了保证引用计数机制正常工作,我们需要正确的调用相关的方法,因此在Netty中对于引用计数方法调用有相关的约定原则,Netty wiki给出的建议是:谁最后访问引用计数对象,谁就负责销毁它。具体有下面的各种情况(可以阅读参考文章的[2]和[3]):

4.1 常规操作

  • 初始值为1
ByteBuf buf = ctx.alloc().directBuffer();
assert buf.refCnt() == 1;
  • release 计数减一:当引用计数为0时,对象会被销毁或者归还到对象池
assert buf.refCnt() == 1;
// release() returns true only if the reference count becomes 0.
boolean destroyed = buf.release();
assert destroyed;
assert buf.refCnt() == 0;
  • retain 计数器加一;只要对象尚未销毁即可加一(为0后不能再加)
ByteBuf buf = ctx.alloc().directBuffer();
assert buf.refCnt() == 1;

buf.retain();
assert buf.refCnt() == 2;

boolean destroyed = buf.release();
assert !destroyed;
assert buf.refCnt() == 1;
  • 无效访问:访问一个引用计数为0的对象将抛出:IllegalReferenceCountException
assert buf.refCnt() == 0;
try {
  buf.writeLong(0xdeadbeef);
  throw new Error("should not reach here");
} catch (IllegalReferenceCountExeception e) {
  // Expected
}

4.2 销毁

4.2.1 销毁时机

按照经验,谁最后一次访问引用计数对象,谁负责销毁它。

1.一个组件将引用计数对象传递给另一个组件,通常发送的组件不需要销毁引用计数,而是延迟由接收组件来完成
2.如果某个组件使用了一个引用计数对象,并且知道没有其他东西可以再访问它了(即,没有将引用传递给另一个组件),则该组件应该销毁它。
4.2.2 派生缓冲区
  • ByteBuf.duplicate(), ByteBuf.slice() and ByteBuf.order(ByteOrder) 方法会创建一个派生缓冲区,缓冲区和parent使用相同的内存区域,派生缓冲器没有自己独立的引用计数,而是共享parent的引用计数
ByteBuf parent = ctx.alloc().directBuffer();
ByteBuf derived = parent.duplicate();

// Creating a derived buffer does not increase the reference count.
assert parent.refCnt() == 1;
assert derived.refCnt() == 1;
  • 需要注意的是:创建派生缓冲器,引用计数并不会加一,因此如果将一个派生缓冲器传递给另一个组件,需要先调用retain()来自增引用计数
ByteBuf parent = ctx.alloc().directBuffer(512);
parent.writeBytes(...);

try {
    while (parent.isReadable(16)) {
        ByteBuf derived = parent.readSlice(16);
        derived.retain();
        process(derived);
    }
} finally {
    parent.release();
}
...

public void process(ByteBuf buf) {
    ...
    buf.release();
}
  • ByteBuf.copy() 和 ByteBuf.readBytes(int) 这样的方法不是使用派生缓冲区,分配并返回的ByteBuf需要释放

4.3 入站出站处理器

4.3.1 入站处理器和引用计数
  • 如果一个事件循环读取到数据到 ByteBuf 并且触发 channelRead 事件,那么 pipeline 对应的 ChannelHandler 需要去释放,因此接收到对应ByteBuf的handler 需要在 channelRead 方法里面释放,代码如下:
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf buf = (ByteBuf) msg;
    try {
        ...
    } finally {
        buf.release();
    }
}
  • 按照前面说的,如果一个 handler只是把 ByteBuf传递给下一个 handler,那么就不需要释放
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf buf = (ByteBuf) msg;
    ...
    ctx.fireChannelRead(buf);
}
  • 需要注意的是: ByteBuf 并不是唯一的引用计数对象,如果在处理消息解码器生成的消息,那么消息也很可能是引用计数类型的:
// Assuming your handler is placed next to `HttpRequestDecoder`
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    if (msg instanceof HttpRequest) {
        HttpRequest req = (HttpRequest) msg;
        ...
    }
    if (msg instanceof HttpContent) {
        HttpContent content = (HttpContent) msg;
        try {
            ...
        } finally {
            content.release();
        }
    }
}
  • 如果你怀疑或者想简化释放消息,可以使用 ReferenceCountUtil.release(): 方法
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    try {
        ...
    } finally {
        ReferenceCountUtil.release(msg);
    }
}
  • 或者,你可以继承 SimpleChannelHandler ,SimpleChannelHandler 使用ReferenceCountUtil.release(msg) 来释放消息
4.3.2 出站处理器和引用计数
  • 和入站处理器不同,出站消息是由应用创建的,因此写到对端之后,由 Netty 负责释放消息,中间拦截出站消息的处理器需要确保正确的释放中间消息
/ Simple-pass through
public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) {
    System.err.println("Writing: " + message);
    ctx.write(message, promise);
}

// Transformation
public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) {
    if (message instanceof HttpContent) {
        // Transform HttpContent to ByteBuf.
        HttpContent content = (HttpContent) message;
        try {
            ByteBuf transformed = ctx.alloc().buffer();
            ....
            ctx.write(transformed, promise);
        } finally {
            content.release();
        }
    } else {
        // Pass non-HttpContent through.
        ctx.write(message, promise);
    }
}

4.4 内存泄露故障排除

4.4.1 泄露检测
  • buffer 泄露故障排除: 引用计数的缺点是容易导致应用计数对象内存泄露,因为JVM并不能感知Netty实现的引用计数,一旦对应对象不可访问就会被GC掉,即使引用计数不是0。一个对象被GC后无法被恢复,这回导致其无法被还原到对象池,从而导致内存泄露
  • 幸运的是,尽管Netty很难找到泄漏,但它默认会对大约1%的缓冲区分配进行采样,以检查应用程序中是否存在泄漏。如果发生泄漏,您将发现以下日志消息:
LEAK: ByteBuf.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 '-Dio.netty.leakDetectionLevel=advanced' or call ResourceLeakDetector.setLevel()
  • 使用上面提到的JVM选项(-Dio.netty.leakDetectionLevel=advanced)重新启动应用程序,然后您将看到应用程序最近访问泄漏缓冲区的位置。以下输出显示了单元测试(XmlFrameDecoderTest.testDecodeWithXml())的泄漏:

  • 以下示例显示泄漏的缓冲区由名为 EchoServerHandler#0 的处理程序处理,然后进行垃圾回收,这意味着EchoServerHandler#0可能忘记释放缓冲区:

12:05:24.374 [nioEventLoop-1-1] ERROR io.netty.util.ResourceLeakDetector - LEAK: ByteBuf.release() was not called before it's garbage-collected.
Recent access records: 2
#2:
	Hint: 'EchoServerHandler#0' will handle the message from this point.
	io.netty.channel.DefaultChannelHandlerContext.fireChannelRead(DefaultChannelHandlerContext.java:329)
	io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:846)
	io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:133)
	io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:485)
	io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:452)
	io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:346)
	io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:794)
	java.lang.Thread.run(Thread.java:744)
#1:
	io.netty.buffer.AdvancedLeakAwareByteBuf.writeBytes(AdvancedLeakAwareByteBuf.java:589)
	io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:208)
	io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:125)
	io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:485)
	io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:452)
	io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:346)
	io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:794)
	java.lang.Thread.run(Thread.java:744)
Created at:
	io.netty.buffer.UnpooledByteBufAllocator.newDirectBuffer(UnpooledByteBufAllocator.java:55)
	io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:155)
	io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:146)
	io.netty.buffer.AbstractByteBufAllocator.ioBuffer(AbstractByteBufAllocator.java:107)
	io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:123)
	io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:485)
	io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:452)
	io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:346)
	io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:794)
	java.lang.Thread.run(Thread.java:744)

  • 泄露检测级别,一共有四个不同的内存泄露检测级别,通过参数指定:java -Dio.netty.leakDetection.level=advanced
DISABLED:关闭,不建议
SIMPLE:1%采样检测是否有内存泄露  
ADVANCED:1%采样,告诉我们内存泄露的位置 
PARANOID:和 ADVANCED 类似,但是会针对每一个buffer去检测,自动化测试阶段很有用,如果输出包含LEAK,则很可能有内存泄露
4.4.2 最佳实践
  • 避免内存泄露的最佳实践
1.在 PARANOID 检测级别运行单元测试和集成测试    
2.在相当长的一段时间内以简单级别部署到整个集群之前,先对应用程序进行Canary处理,以查看是否存在泄漏。
3.如果有泄露 ,再次  以获取一些关于哪里内存泄露的线索
4.不要将内存泄露的应用部署到整个集群
  • 单元测试修复内存泄露:在单元测试中忘记释放缓冲区或消息是非常容易的。它将生成泄漏警告,但这并不一定意味着您的应用程序存在泄漏。可以使用ReferenceCountUtil.releaseLater方法,而不是使用try-finally块包装单元测试代码来释放所有缓冲区
import static io.netty.util.ReferenceCountUtil.*;

@Test
public void testSomething() throws Exception {
    // ReferenceCountUtil.releaseLater() will keep the reference of buf,
    // and then release it when the test thread is terminated.
    ByteBuf buf = releaseLater(Unpooled.directBuffer(512));
    ...
}

参考

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值