泄露监测

背景

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);
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,843评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,538评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,187评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,264评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,289评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,231评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,116评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,945评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,367评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,581评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,754评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,458评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,068评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,692评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,842评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,797评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,654评论 2 354

推荐阅读更多精彩内容