Netty如何监控内存泄露
前言
在前面的四个章节中,我们介绍了 Netty 中用于内存管理的内存池算法。Netty4 开始默认都使用内存池用于分配内存空间。而承载池化内存空间的一般都是PooledByteBuf对象。使用池化ByteBuf可以提高性能,但是使用完毕需要注意是否需要进行手工释放。在需要手动释放的场合没有释放,就会因为申请的内存空间没有归还给内存池造成内存泄漏,最终应用程序OOM。
在复杂的应用程序中找到没有释放的PooledByteBuf是一个比较困难的事情,在没有工具辅助的情况下只能白盒检查所有的代码,效率无疑十分低下。
好在 Netty 也考虑到了这种情况,在 Netty 中设计了专门的泄漏检测接口用于对需要手动释放的资源对象进行监控。
JDK的弱引用和引用队列
在分析Netty的泄露监控功能之前,先来复习下其中会用到的JDK知识:引用。
在java中存在4中引用类型,分别是强引用,软引用,弱引用,虚引用。
强引用
强引用,是我们写程序最经常使用的方式。比如将一个值赋给一个变量,那这个对象就被该变量强引用了。除非将将null设置给该变量,否则因为该变量一直引用着对象,java的内存回收不会回收该对象。就算是内存不足异常发生也不会。
软引用
软引用所引用的对象会在java内存不足的时候,被gc回收。如果gc发生的时候,java的内存还充足则不会回收这个对象
使用的方式如下
SoftReference ref = new SoftReference(new Date()); Date tmp = ref.get(); //如果对象没有被回收,则这个get操作会返回初始化的值。如果被回收了之后,则返回null
弱引用
弱引用则比软引用更差一些。只要是gc发生的时候,弱引用的对象都会被回收。使用方式上和软引用类似,如下
WeakReference re = new WeakReference(new Date()); re.get();
虚引用
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在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();
软引用是在JVM内存不足的时候才会执行回收,较为适合的使用场景是做JVM实例内缓存。
引用队列的一个适用场景:与弱引用或虚引用配合,监控一个对象是否被GC回收。
Netty的实现思路
针对需要手动关闭的资源对象,Netty设计了一个接口ResourceLeakTracker来实现对资源对象的追踪。该接口定义了三个方法,分别如下
void record();//记录当前的调用信息,实际上相当于记录了追踪对象当前的调用轨迹 void record(Object hint);//记录当前的调用信息,并且额外记录了提示信息,也就是hint boolean close(T trackedObject);//停止追踪trackedObject对象
被追踪的资源对象,在手动释放自身的时候都需要调用方法close来改变ResourceLeakTracker的状态。而在close之前,可以通过record方法来记录该被追踪的资源对象在代码的什么地方被调用过。
该接口只有唯一一个实现DefaultResourceLeak,该实现继承了WeakReference。每一个DefaultResourceLeak会与一个需要监控的资源对象关联,同时关联着一个引用队列。
当资源对象被GC回收后,与之关联的DefaultResourceLeak就会进入引用队列。通过检查引用队列中的DefaultResourceLeak实例的状态(close方法的调用会导致状态变更),就能确定在资源对象被GC前,是否执行了手动关闭的相关方法,从而判断是否存在泄漏可能。
代码实现
DefaultResourceLeak
首先来看下追踪对象DefaultResourceLeak的实现,先查看其拥有的属性,如下
private volatile Record head;//Record对象形成一个后进先出的链表,head属性指向最后一次调用record方法存储的调用记录 private volatile int droppedRecords;//记录被丢弃的record对象个数 private final Set < DefaultResourceLeak <? >> allLeaks; //存储所有追踪对象实例 private final int trackedHash;//被追踪的资源对象的hash值,该值用于在close方法调用时进行比对,避免不正确的执行close
再来看下该类的类图,如下

可以看到,一方面实现了接口,一方面继承了弱引用,和前面的思路就对的上。再来看看其构造方法,如下
DefaultResourceLeak(Object referent, ReferenceQueue < Object > refQueue, Set < DefaultResourceLeak <? >> allLeaks)
{
super(referent, refQueue);
trackedHash = System.identityHashCode(referent);
allLeaks.add(this);
headUpdater.set(this, new Record(Record.BOTTOM));
this.allLeaks = allLeaks;
}
上文说接口的时候提到方法close,为了避免错误的调用这个方法,因此这个方法的入参是资源对象,该入参的资源对象和追踪对象存储的进行对比,以确保关闭方法是针对一开始追踪的资源对象。但是在构造方法中,我们注意到,没有使用一个变量存储一开始要追踪的资源对象,而是使用了System.identityHashCode获取了hashcode来作为close方法比对的依据。如果这里存储了referent,就会导致资源对象无法被GC。因为存在着一个强引用链,如下
io.netty.buffer.AbstractByteBuf#leakDetector --> io.netty.util.ResourceLeakDetector#allLeaks
-->io.netty.util.ResourceLeakDetector.DefaultResourceLeak --> 资源对象
因此这里只能通过别的方式保留资源对象特征,也就是hashcode值。
在DefaultResourceLeak的属性中,我们看到一个类ResourceLeakDetector.Record。每一次调用DefaultResourceLeak#record()方法时,就会生成一个Record实例,该实例记录了当前的堆栈信息。我们来看下Record的定义,如下
private static final class Record extends Throwable
{
private static final long serialVersionUID = 6065153674892850720 L;
private static final Record BOTTOM = new Record();
private final String hintString;
private final Record next;
private final int pos;
}
可以看到,Record对象继承了Throwable,而Throwable的默认构造方法中会调用方法Throwable#fillInStackTrace(),该方***通过本地方法调用,将当前的线程堆栈信息生成并填充到Throwable对象中,从而保存了该对象被生成时候的堆栈信息。
Record继承了Throwable并且自身通过next形成了一个链表,这实际上暗示了这个链表就是用于存储DefaultResourceLeak在各个不同的调用处采集到的调用堆栈信息。
从思路到构造方法,我们可以梳理出DefaultResourceLeak的使用方式。当资源对象生成的时候,创建一个DefaultResourceLeak并且指向该资源对象。同时资源对象可以指持有DefaultResourceLeak对象。资源对象在各个不同地方的代码使用的时候,调用DefaultResourceLeak的record方法来记录资源对象的使用轨迹。最终在资源对象被GC回收时,在引用队列中查看DefaultResourceLeak是否被调用过close方法,如果调用过,则说明资源对象已经被释放了;如果没有的话,则意味着资源对象是直接被GC回收的,在回收之前不曾手动释放过,存在内存泄漏的隐患。
分配监控对象
看过了DefaultResourceLeak的构造方法信息后,也了解了其使用方式,下面我们来看下其具体的生成时机。我们知道在进行内存分配的时候,我们依赖的入口是内存分配器,ByteBufAllocator#buffer()类方法。只有池化的分配器,采用了内存池,才需要内存追踪。我们来看其实现,如下
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实例生成并且初始化完毕,持有了内存区域后,在返回给开发者使用之前,尝试为其生成一个代理,用于资源泄漏追踪,也就是方法toLeakAwareBuffer,代码如下
protected static ByteBuf toLeakAwareBuffer(ByteBuf buf)
{
ResourceLeakTracker < ByteBuf > leak;
switch(ResourceLeakDetector.getLevel())
{
case SIMPLE:
leak = AbstractByteBuf.leakDetector.track(buf);
if
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
<p> 通过本专刊的学习,对网络开发所需掌握的基础理论知识会更加牢固,对网络应用涉及的线程模型,设计模式,高性能架构等更加明确。通过对Netty的源码深入讲解,使得读者对Netty达到“知其然更之所以然”的程度。在遇到一些线上的问题时,具备了扎实理论功底的情况,可以有的放矢而不会显得盲目。 本专刊购买后即可解锁所有章节,故不可以退换哦~ </p> <p> <br /> </p>

