[TOC]java
通常而言,在Netty程序中都会采用池化的ByteBuf,也就是PooledByteBuf
以提升程序性能。可是PooledByteBuf
须要在使用完毕后手工释放,不然就会由于PooledByteBuf
申请的内存空间没有归还进而形成内存泄露,最终OOM。而一旦泄露发生,在复杂的应用程序中找到未手工释放的ByteBuf
并非一个简单的活计,在没有工具辅助的状况只能白盒检查全部源码,效率无疑十分低下。dom
为了解决这个问题,Netty设计了专门的泄露检测接口用于实现对须要手动释放的资源对象的监控。ide
在分析Netty的泄露监控功能以前,先来复习下其中会用到的JDK知识:引用。工具
在java中存在4中引用类型,分别是强引用,软引用,弱引用,虚引用。性能
强引用this
强引用,是咱们写程序最常用的方式。好比一个将一个值赋给一个变量,那这个对象值就被该变量强引用了。除非设置为null,不然java的内存回收不会回收该对象。就算是内存不足异常发生也不会。.net
软引用设计
软引用所引用的对象会在java内存不足的时候,被gc回收。若是gc发生的时候,java的内存还充足则不会回收这个对象 使用的方式以下指针
弱引用日志
弱引用则比软引用更差一些。只要是gc发生的时候,弱引用的对象都会被回收。使用方式上和软引用相似,以下
虚引用
虚引用和前面的软引用、弱引用不一样,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference
类表示。若是一个对象与虚引用关联,则跟没有引用与之关联同样,在任什么时候候均可能被垃圾回收器回收。
除了强引用以外,其他的引用都有一个引用队列能够与之配合。当java清理调用没必要要的引用后,会将这个引用自己(不是引用指向的值对象)添加到队列之中。代码以下
ReferenceQueue<Date> queue = new ReferenceQueue<>(); WeakReference<Date> re = new WeakReference<Date>(new Date(), queue); Reference<? extends Date> moved = queue.poll();
从上面的介绍能够看出引用队列的一个适用场景:与弱引用或虚引用配合,监控一个对象是否被GC回收。
针对须要手动关闭的资源对象,Netty设计了一个接口io.netty.util.ResourceLeakTracker
来实现对资源对象的追踪。该接口提供了一个release
方法。在资源对象关闭须要调用release
方法。若是从未调用release
方法则被认为存在资源泄露。
该接口只有一个实现,就是io.netty.util.ResourceLeakDetector.DefaultResourceLeak
,该实现继承了WeakReference
。每个DefaultResourceLeak
会与一个须要监控的资源对象关联,同时关联着一个引用队列。
当资源对象被GC回收后,与之关联的DefaultResourceLeak
就会进入引用队列。经过检查引用队列中的DefaultResourceLeak
实例的状态(release
方法的调用会致使状态变动),就能肯定在资源对象被GC前,是否执行了手动关闭的相关方法,从而判断是否存在泄漏可能。
当进行ByteBuf的分配的时候,好比方法io.netty.buffer.PooledByteBufAllocator#newHeapBuffer
,查看代码以下
protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) { PoolThreadCache cache = threadCache.get(); PoolArena<byte[]> heapArena = cache.heapArena; final ByteBuf buf; if (heapArena != null) { buf = heapArena.allocate(cache, initialCapacity, maxCapacity); } else { buf = PlatformDependent.hasUnsafe() ? new UnpooledUnsafeHeapByteBuf(this, initialCapacity, maxCapacity) : new UnpooledHeapByteBuf(this, initialCapacity, maxCapacity); } return toLeakAwareBuffer(buf); }
当实际持有内存区域的ByteBuf
生成,经过方法io.netty.buffer.AbstractByteBufAllocator#toLeakAwareBuffer(io.netty.buffer.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; }
根据不一样的监控级别生成不一样的监控等级对象。Netty对监控分为4个等级:
通常而言,在项目的初期使用简单模式进行监控,若是没有问题一段时间后就能够关闭。不然升级到加强或者偏执模式尝试确认泄露位置。
泄露的检查和追踪主要依靠两个类io.netty.util.ResourceLeakDetector.DefaultResourceLeak
和io.netty.util.ResourceLeakDetector
.前者用于追踪一个资源对象,而且记录对应的调用轨迹;后者则负责管理和生成DefaultResourceLeak
对象。
首先来看用于追踪资源对象的监控对象。该类继承了WeakReference
,有几个重要的属性,以下
//存储着最新的调用轨迹信息,record内部经过next指针造成一个单向链表 private volatile Record head; //调用轨迹不会无限制的存储,有一个上限阀值。超过了阀值会抛弃掉一些调用轨迹信息。 private volatile int droppedRecords; //存储着全部的追踪对象,用于确认追踪对象是否处于可用。 private final Set<DefaultResourceLeak<?>> allLeaks; //记录追踪对象的hash值,用于后续操做中的对象对比。 private final int trackedHash;
这个类的做用有三个:
WeakReference
,在追踪对象被GC回收后自身被入列到ReferenceQueue
中。先来看下record
方法,代码以下
@Override public void record() { record0(null); } @Override public void record(Object hint) { record0(hint); } private void record0(Object hint) { if (TARGET_RECORDS > 0) { Record oldHead; Record prevHead; Record 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; } newHead = hint != null ? new Record(prevHead, hint) : new Record(prevHead); } while (!headUpdater.compareAndSet(this, oldHead, newHead)); if (dropped) { droppedRecordsUpdater.incrementAndGet(this); } } }
方法record0
的思路总结下也很简单,归纳以下:
Record
对象中的pos属性记录着当前轨迹链的长度,当追踪对象的轨迹队链的长度超过配置值时,有必定的概率(1-1/2<sup>min(n-target_record,30)</sup>)将最新的轨迹对象从链条中删除。步骤2中在链条过长时选择删除最新的轨迹对象是基于如下两点出发:
在来看看close
方法。代码以下
public boolean close(T trackedObject) { assert trackedHash == System.identityHashCode(trackedObject); try { return close(); } finally { reachabilityFence0(trackedObject); } } public boolean close() { if (allLeaks.remove(this)) { // Call clear so the reference is not even enqueued. clear(); headUpdater.set(this, null); return true; } return false; } private static void reachabilityFence0(Object ref) { if (ref != null) { synchronized (ref) { } } }
close
方法自己没有什么,就是将资源进行了清除。须要解释的是方法reachabilityFence0
。不过该方法须要在下文的报告泄露中才会具有做用,这边先暂留。
该类用于按照规则进行追踪对象的生成,外部主要是调用其方法track
,代码以下
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; } if (level.ordinal() < Level.PARANOID.ordinal()) { if ((PlatformDependent.threadLocalRandom().nextInt(samplingInterval)) == 0) { reportLeak(); return new DefaultResourceLeak(obj, refQueue, allLeaks); } return null; } reportLeak(); return new DefaultResourceLeak(obj, refQueue, allLeaks); }
从生成策略来看,只要是小于PARANOID
级别都是抽样生成。生成的追踪对象上一个章节已经分析过了,这边主要来看reportLeak
方法,以下
private void reportLeak() { if (!logger.isErrorEnabled()) { clearRefQueue(); return; } // Detect and report previous leaks. for (;;) { @SuppressWarnings("unchecked") DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll(); if (ref == null) { break; } //返回true意味着资源没有调用close或者dispose方法结束追踪就被GC了,意味着该资源存在泄漏。 if (!ref.dispose()) { continue; } String records = ref.toString(); if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) { if (records.isEmpty()) { reportUntracedLeak(resourceType); } else { reportTracedLeak(resourceType, records); } } } } boolean io.netty.util.ResourceLeakDetector.DefaultResourceLeak#dispose() { clear(); return allLeaks.remove(this); }
能够看到,每次生成资源追踪对象时,都会遍历引用队列,若是发现泄漏对象,则进行日志输出。
这里面有个细节的设计点在于DefaultResourceLeak
进入引用队列并不意味着必定内存泄露。判断追踪对象是否泄漏的规则是对象在被GC以前是否调用了DefaultResourceLeak
的close
方法。举个例子,PooledByteBuf
只要将自身持有的内存释放回池化区就算是正确的释放,其后其实例对象能够被GC回收掉。
所以方法reportLeak
在遍历引用队列时,须要经过调用dispose
方法来确认追踪对象的dispose
是否调用或者close
方法是否被调用过。若是dispose
方法返回true,则意味着被追踪对象未调用关闭方法就被GC,那就意味着形成了泄露。
上个章节曾提到的一个方法reachabilityFence0
。
在JVM的规定中,若是一个实例对象再也不被须要,则能够断定为可回收。即便该实例对象的一个具体方法正在执行过程当中,也是能够的。更确切一些的说,若是一个实例对象的方法体中,再也不须要读取或者写入实例对象的属性,则此时JVM能够回收该对象,即便方法尚未完成。
然而这样会致使一个问题,在close方法中,若是close方法尚未执行完毕,trackedObject
对象实例就被GC回收了,就会致使DefaultResourceLeak
对象被加入到引用队列中,从而可能在reportLeak
方法调用中触发方法dispose
,假设此时close
方法才刚开始执行,则dispose
方法可能返回true。程序就会断定这个对象出现了泄露,然而实际上却没有。
要解决这个问题,只须要让close
方法执行完毕前,让对象不要回收便可。reachabilityFence0
方法就完成了这个做用。
文章原创首发于公众号:林斌说Java,转载请注明来源,谢谢。
欢迎扫码关注