Netty源码解析——Buffer之ByteBuf 内存泄漏检测
0.引用计数器基础知识
1) 计数器基于AtomicIntegerFieldUpdater,因为ByteBuf对象很多,如果都把int包一层AtomicInteger花销较大,而AtomicIntegerFieldUpdater只需要一个全局的静态变量。所有的ByteBuf的引用计数器初始值为1.
private static final AtomicIntegerFieldUpdater<AbstractReferenceCounted> refCntUpdater =
AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCounted.class, "refCnt");
private volatile int refCnt = 1;
拓展:AtomicIntegerFieldUpdater:允许类中里面一个字段具有原子性。这个字段必须是volatile类型的,在线程间共享变量时保证其可见性.字段描述类型是与调用者与操作对象字段的关系一致,可以进行反射进行原子操作.对于父类的字段,子类不能直接操作,只能是实例变量和可修改的变量,不能再被修饰的类前面加 static 和final 的修饰符. 比如refCnt不能加static和final修饰符,如果是包装类Integer和Long的化可以使用AtomicReferenceFieldUpdater类.
2) release()和retain() 分别对应着计数器refCnt加1和减1.等于零时deallocate()被调用进行回收.由于每次操作只能操作的数为1,居然在程序里面直接判断old值为1时就进行deallocate(),为什么这么写?当引用计数器为0时,底下的buffer已经回收,即使ByteBuf对象还在,对它的各种访问操作都会抛出异常。
3) 有duplicate(),slice(),order()所衍生出来的ByteBuf与原对象共享底下的buffer,也共享引用计数器,所以它们经常需要调用retain()来显示自己的存在。copy()方法生成的ByteBuf完全独立于原ByteBuf,而slice()和duplicate()方法生成的ByteBuf与原ByteBuf共享相同的底层实现,但是各自维护独立的索引和标记.slice()方法从原始的ByteBuf中截取一段,这段数据从readerIndex到writeIndex,返回时新的ByteBuf的最大容量maxCapacity为原始ByteBuf的readableBytes().而duplicate()则是把整个ByteBuf截取出来.在一个函数体里面,只要增加了引用计数就必须调用release()方法.
1.Netty内存泄漏检测
内存泄漏,主要是针对池化的ByteBuf.ByteBuf对象被JVM GC掉之前,没有调用release()把底下的DirectByteBuffer或byte[]归还到池里,会导致池越来越大.Netty默认会从分配的ByteBuf里抽样出大约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()
禁用(DISABLED) - 完全禁止泄露检测,省点消耗。
简单(SIMPLE) - 默认等级,告诉我们取样的1%的ByteBuf是否发生了泄露,但总共一次只打印一次,看不到就没有了。
高级(ADVANCED) - 告诉我们取样的1%的ByteBuf发生泄露的地方。每种类型的泄漏(创建的地方与访问路径一致)只打印一次。对性能有影响。
偏执(PARANOID) - 跟高级选项类似,但此选项检测所有ByteBuf,而不仅仅是取样的那1%。对性能有绝大的影响。
ByteBufAllocator 可用于创建 ByteBuf 对象。创建的过程中,它会调用 #toLeakAwareBuffer(...) 方法,将 ByteBuf 装饰成 LeakAware ( 可检测内存泄露 )的 ByteBuf 对象.
1.1 ResourceLeakDetector
ResourceLeakDetector 为了检测内存是否泄漏,使用了 WeakReference( 弱引用 )和 ReferenceQueue( 引用队列 ),过程如下:
根据检测级别和采样率的设置,在需要时为需要检测的 ByteBuf 创建WeakReference 引用。
当 JVM 回收掉 ByteBuf 对象时,JVM 会将 WeakReference 放入ReferenceQueue 队列中。
通过对 ReferenceQueue 中 WeakReference 的检查,判断在 GC 前是否有释放ByteBuf 的资源,就可以知道是否有资源释放。
private final ConcurrentMap<DefaultResourceLeak<?>, LeakEntry> allLeaks = PlatformDependent.newConcurrentHashMap();//DefaultResourceLeak集合
private final ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();//引用队列
private final ConcurrentMap<String, Boolean> reportedLeaks = PlatformDependent.newConcurrentHashMap();//已汇报的内存泄露的资源类型的集合
private final String resourceType;//资源类型
private final int samplingInterval;//采样频率
1.2 ResourceLeakTracker
每个资源( 例如:ByteBuf 对象 ),会创建一个追踪它是否内存泄露的 ResourceLeakTracker 对象,代码如下
leak = AbstractByteBuf.leakDetector.track(buf);
if (leak != null) {
buf = new AdvancedLeakAwareByteBuf(buf, leak);
}
1.3 DefaultResourceLeak
DefaultResourceLeak 继承 java.lang.ref.WeakReference 类,实现 ResourceLeakTracker 接口,默认 ResourceLeakTracker 实现类。同时,它是 ResourceLeakDetector 内部静态类.
private static final class DefaultResourceLeak<T>
extends WeakReference<Object> implements ResourceLeakTracker<T>, ResourceLeak
构造函数
DefaultResourceLeak(
Object referent,
ReferenceQueue<Object> refQueue,
ConcurrentMap<DefaultResourceLeak<?>, LeakEntry> allLeaks) {
super(referent, refQueue);
trackedHash = System.identityHashCode(referent);
allLeaks.put(this, LeakEntry.INSTANCE);
// Create a new Record so we always have the creation stacktrace included.
headUpdater.set(this, new Record(Record.BOTTOM));
this.allLeaks = allLeaks;
}
[拓展]:引用队列(Reference Queue)
一旦弱引用对象开始返回null,该弱引用指向的对象就被标记成了垃圾。而这个弱引用对象(非其指向的对象)就没有什么用了。通常这时候需要进行一些清理工作。比如WeakHashMap会在这时候移除没用的条目来避免保存无限制增长的没有意义的弱引用。
引用队列可以很容易地实现跟踪不需要的引用。当你在构造WeakReference时传入一个ReferenceQueue对象,当该引用指向的对象被标记为垃圾的时候,这个引用对象会自动地加入到引用队列里面。接下来,你就可以在固定的周期,处理传入的引用队列,比如做一些清理工作来处理这些没有用的引用对象。
当referent为 ByteBuf 对象时,如果它被正确的释放,即调用了release()方法,从而调用了 AbstractReferenceCountedByteBuf 的closeLeak()方法,最终调用到 ResourceLeakTracker#close(trackedByteBuf)方法,那么该 ByteBuf 对象对应的 ResourceLeakTracker 对象,将从ResourceLeakDetector.allLeaks中移除。在ResourceLeakDetector#reportLeak()方法中,即使从refQueue队列中,获取到该 ByteBuf 对象对应 ResourceLeakTracker 对象,因为在ResourceLeakDetector.allLeaks中移除了,所以在ResourceLeakDetector#reportLeak()!ref.dispose() = true,continue 就不报告内存泄漏了。