泄露监测

2019-08-01  本文已影响0人  Pillar_Zhong

背景

Netty中的ByteBuf是make things right的关键,对象本身可以被对象池回收,而它所占据的内存空间也可以被回收再分配,而这一切都是通过调用release来达成。

自从Netty 4开始,对象的生命周期由它们的引用计数管理,而不是由垃圾收集器管理了。Netty的原意是当引用计数归零才需要去release, 由于JVM并没有意识到Netty实现的引用计数对象,它仍会将这些引用计数对象当做常规对象处理,也就意味着,当不为0的引用计数对象变得不可达时仍然会被GC自动回收。一旦被GC回收,那么意味着该死的release我永远都无法触达,这样便会造成内存泄露。举个实际的经常犯的毛病, ByteBuf用完忘记release. 如果没有一定的机制, 你可能永远都发现不了.

当然, Netty的方案并没有给社区提供包山包海通天的解决方案, 他是根据设定的频率来检测可能的泄漏, 最终通过日志告知开发者有泄露,要求开发者来排查问题。

引用

在深入Netty的解决方案前, 我们有必要先回顾下Java的几种引用类型.

  1. 强引用,最普遍的引用,类似Object obj = new Object()这类的引用。只要强引用还存在,垃圾回收器就不会回收掉被引用的对象。当内存空间不足,JVM宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
  2. 软引用(SoftReference类),如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它,而如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的缓存。
  3. 弱引用(WeakReference类),弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。垃圾回收器进行对象扫描时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
  4. 虚/幻影引用(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);
    }
}
上一篇下一篇

猜你喜欢

热点阅读