美文网首页
泄露监测

泄露监测

作者: Pillar_Zhong | 来源:发表于2019-08-01 17:15 被阅读0次

    背景

    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);
        }
    }
    

    相关文章

      网友评论

          本文标题:泄露监测

          本文链接:https://www.haomeiwen.com/subject/jyecdctx.html