背景
Netty中的ByteBuf是make things right的关键,对象本身可以被对象池回收,而它所占据的内存空间也可以被回收再分配,而这一切都是通过调用release来达成。
自从Netty 4开始,对象的生命周期由它们的引用计数管理,而不是由垃圾收集器管理了。Netty的原意是当引用计数归零才需要去release, 由于JVM并没有意识到Netty实现的引用计数对象,它仍会将这些引用计数对象当做常规对象处理,也就意味着,当不为0的引用计数对象变得不可达时仍然会被GC自动回收。一旦被GC回收,那么意味着该死的release我永远都无法触达,这样便会造成内存泄露。举个实际的经常犯的毛病, ByteBuf用完忘记release. 如果没有一定的机制, 你可能永远都发现不了.
当然, Netty的方案并没有给社区提供包山包海通天的解决方案, 他是根据设定的频率来检测可能的泄漏, 最终通过日志告知开发者有泄露,要求开发者来排查问题。
引用
在深入Netty的解决方案前, 我们有必要先回顾下Java的几种引用类型.
- 强引用,最普遍的引用,类似
Object obj = new Object()
这类的引用。只要强引用还存在,垃圾回收器就不会回收掉被引用的对象。当内存空间不足,JVM宁愿抛出OutOfMemoryError
错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。- 软引用(
SoftReference
类),如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它,而如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的缓存。- 弱引用(
WeakReference
类),弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。垃圾回收器进行对象扫描时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。- 虚/幻影引用(
PhantomReference
类),虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。而且也无法通过虚引用来取得一个对象实例。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列ReferenceQueue中。
假如
当我们的资源被GC时, Phantom Reference队列能取到指向它的 PhantomReference. 前提是这个PhantomReference不能是孤立的, 不然会被GC掉. 解决办法也很简单粗暴, 我们需要提供一个容器来托管他们, 只要容器不倒, 他们就不会消失. 一旦该资源被成功release, 那么立即从这个容器中移除掉. 那么该资源的PhantomReference不久就会被GC掉.
泄露监测
protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
PoolThreadCache cache = threadCache.get();
PoolArena<byte[]> heapArena = cache.heapArena;
ByteBuf buf;
if (heapArena != null) {
buf = heapArena.allocate(cache, initialCapacity, maxCapacity);
} else {
buf = new UnpooledHeapByteBuf(this, initialCapacity, maxCapacity);
}
// 在新建ByteBuf的时候, 会开始监控该buf是否会泄漏
return toLeakAwareBuffer(buf);
}
// 装饰器模式, 对现有buf的增强
protected static ByteBuf toLeakAwareBuffer(ByteBuf buf) {
ResourceLeak leak;
switch (ResourceLeakDetector.getLevel()) {
// 至于下面的level不是重点, 内存泄漏的监控也是要成本的, 就看怎么取舍
// 而不同的level都会去到AbstractByteBuf.leakDetector.open
// 这里很形象,就是告诉leakDetector我要检测这个对象, 如果发生泄漏上报给我.
case SIMPLE:
leak = AbstractByteBuf.leakDetector.open(buf);
if (leak != null) {
buf = new SimpleLeakAwareByteBuf(buf, leak);
}
break;
case ADVANCED:
case PARANOID:
leak = AbstractByteBuf.leakDetector.open(buf);
if (leak != null) {
buf = new AdvancedLeakAwareByteBuf(buf, leak);
}
break;
default:
break;
}
return buf;
}
public final ResourceLeak open(T obj) {
Level level = ResourceLeakDetector.level;
if (level == Level.DISABLED) {
return null;
}
if (level.ordinal() < Level.PARANOID.ordinal()) {
// 每隔128次泄漏检查就要出具报告一次
if ((++ leakCheckCnt & mask) == 0) {
reportLeak(level);
return new DefaultResourceLeak(obj);
} else {
return null;
}
} else {
reportLeak(level);
return new DefaultResourceLeak(obj);
}
}
private void reportLeak(Level level) {
// 首先你的日志级别要是error, 否则将refQueue里面对象全部清掉
// 换句话说, 只要日志不对, 泄漏检测就什么都不做
if (!logger.isErrorEnabled()) {
for (;;) {
@SuppressWarnings("unchecked")
DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
if (ref == null) {
break;
}
ref.close();
}
return;
}
// 如果你申请监控的资源对象太多也要提醒开发者.
int samplingInterval = level == Level.PARANOID? 1 : this.samplingInterval;
if (active * samplingInterval > maxActive && loggedTooManyActive.compareAndSet(false, true)) {
reportInstancesLeak(resourceType);
}
// 遍历refQueue
for (;;) {
@SuppressWarnings("unchecked")
DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
if (ref == null) {
break;
}
// 这里保证ref不会再回到refQueue里面
ref.clear();
// 这里是将DefaultResourceLeak从队列中删除, 也就是从观察名单中移除
if (!ref.close()) {
continue;
}
// 接下来就是生成这个资源对象的泄露报告了
// 这里的records主要是该资源在每次retain的时候,视情况去记录轨迹,说白了就是使用记录
// 如果返回空,那么只上报基本情况,否则将轨迹一起上报.
// 里面很简单, 就不再深入了
String records = ref.toString();
if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) {
if (records.isEmpty()) {
reportUntracedLeak(resourceType);
} else {
reportTracedLeak(resourceType, records);
}
}
}
}
容器
关键属性
// 创建记录
private final String creationRecord;
// 引用记录轨迹
private final Deque<String> lastRecords = new ArrayDeque<String>();
// 是否被close
private final AtomicBoolean freed;
// 前驱
private DefaultResourceLeak prev;
// 后继
private DefaultResourceLeak next;
// 从上面就可以看出来这个容器是以DefaultResourceLeak为节点类型的双向链表
// 删除的轨迹记录
private int removedRecords;
DefaultResourceLeak(Object referent) {
// 包装PhantomReference, 捎上refQueue, JVM垃圾回收时会将满足条件的填入queue中
super(referent, referent != null? refQueue : null);
if (referent != null) {
Level level = getLevel();
if (level.ordinal() >= Level.ADVANCED.ordinal()) {
creationRecord = newRecord(null, 3);
} else {
creationRecord = null;
}
// 将该兼容资源假如到这个双向列表中.
synchronized (head) {
prev = head;
next = head.next;
head.next.prev = this;
head.next = this;
active ++;
}
freed = new AtomicBoolean();
} else {
creationRecord = null;
freed = new AtomicBoolean(true);
}
}
网友评论