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