莫道桑榆晚,为霞尚满天。居家隔离的第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的内存是否正常释放,要想实现这个功能,如果让你来设计怎么做呢?
原理类似《把大象装进冰箱拢共分几步》,首先既然要检测,那么就要记录当前有哪些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方法,通过头插法维护当前的调用栈,用来做调用追溯。