Skip to content

netty leak_detector

wangkaiwu edited this page Feb 27, 2023 · 1 revision

netty 资源泄露调查器(ResourceLeakDetector)

在java程序启动时,加入参数 -Dio.netty.leakDetection.level=PARANOID 可以查出 ByteBuf 泄露的位置。从这个参数聊起,看下如何生效的

参数作用

public class ResourceLeakDetector<T> {
    private static final String PROP_LEVEL = "io.netty.leakDetection.level";

    private static final String PROP_TARGET_RECORDS = "io.netty.leakDetection.targetRecords";
    private static final int DEFAULT_TARGET_RECORDS = 4;

    private static final String PROP_SAMPLING_INTERVAL = "io.netty.leakDetection.samplingInterval";
    private static final int DEFAULT_SAMPLING_INTERVAL = 128;

    private static final int TARGET_RECORDS;
    static final int SAMPLING_INTERVAL;
    private static Level level;

    static {
        String levelStr = SystemPropertyUtil.get(PROP_LEVEL, levelStr);
        Level level = Level.parseLevel(levelStr);
        // 默认值是 4
        TARGET_RECORDS = SystemPropertyUtil.getInt(PROP_TARGET_RECORDS, DEFAULT_TARGET_RECORDS);
        // 默认值 128
        SAMPLING_INTERVAL = SystemPropertyUtil.getInt(PROP_SAMPLING_INTERVAL, DEFAULT_SAMPLING_INTERVAL);
        ResourceLeakDetector.level = level;
    }
}

Level 枚举

public enum Level {
    DISABLED,
    SIMPLE,
    ADVANCED,
    PARANOID;
}

level 的使用(查找泄露源)

通过调用 track0 方法可以打印泄露位置

查找泄露源
public class ResourceLeakDetector<T> {
    private final Set<DefaultResourceLeak<?>> allLeaks =
            Collections.newSetFromMap(new ConcurrentHashMap<DefaultResourceLeak<?>, Boolean>());
    private final ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>(); 
    
    private DefaultResourceLeak track0(T obj) {
        Level level = ResourceLeakDetector.level;
        if (level == Level.DISABLED) {
            return null;
        }
        if (level.ordinal() < Level.PARANOID.ordinal()) {
            // 默认1/128的概率随机
            if ((PlatformDependent.threadLocalRandom().nextInt(samplingInterval)) == 0) {
                reportLeak();
                return new DefaultResourceLeak(obj, refQueue, allLeaks);
            }
            return null;
        }
        reportLeak();
        return new DefaultResourceLeak(obj, refQueue, allLeaks);
    }
}
public class ResourceLeakDetector<T> {
    private final ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();

    private void reportLeak() {
        for (; ; ) {
            DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
            if (ref == null) {
                break;
            }
            if (!ref.dispose()) {
                continue;
            }
            String records = ref.toString();
            if (reportedLeaks.add(records)) {
                if (records.isEmpty()) {
                    reportUntracedLeak(resourceType);
                } else {
                    reportTracedLeak(resourceType, records);
                }
            }
        }
    }
}

关于调查泄露源的结果

  • reportUntracedLeak,无法找到泄露源
public class ResourceLeakDetector<T> {
    protected void reportUntracedLeak(String resourceType) {
        logger.error("LEAK: {}.release() was not called before it's garbage-collected. " +
                        "Enable advanced leak reporting to find out where the leak occurred. " +
                        "To enable advanced leak reporting, " +
                        "specify the JVM option '-D{}={}' or call {}.setLevel() " +
                        "See https://netty.io/wiki/reference-counted-objects.html for more information.",
                resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));
    }
}
  • reportTracedLeak,找到泄露源,并打印泄露位置
public class ResourceLeakDetector<T> {
    protected void reportTracedLeak(String resourceType, String records) {
        logger.error(
                "LEAK: {}.release() was not called before it's garbage-collected. " +
                        "See https://netty.io/wiki/reference-counted-objects.html for more information.{}",
                resourceType, records);
    }
}

refQueue 的添加

由于追踪 ByteBuf 时,都会生成一个 DefaultResourceLeak 对象,构造的时候会传入 refQueue 和 allLeaks。

public class ResourceLeakDetector<T> {
    private static final class DefaultResourceLeak<T>
            extends WeakReference<Object> implements ResourceLeakTracker<T>, ResourceLeak {
        private final Set<DefaultResourceLeak<?>> allLeaks;
        DefaultResourceLeak(
                Object referent,
                ReferenceQueue<Object> refQueue,
                Set<DefaultResourceLeak<?>> allLeaks) {
            super(referent, refQueue);
            // .......
            allLeaks.add(this);
            this.allLeaks = allLeaks;
        }
    }
}

根据弱引用的规则,当引用对象被回收的时候,会将引用对象放入 refQueue 中。

public class WeakReference<T> extends Reference<T> {
    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

通过下面的对象可知,每个 ByteBuf 其实都包装了一个强引用的 trackedByteBuf 和一个弱引用的 leak。 一旦强引用对象GCRoot 不可达,则 gc 触发后会同时清理弱引用的 leak 对象,同时置入 refQueue。

class SimpleLeakAwareByteBuf extends WrappedByteBuf {
    private final ByteBuf trackedByteBuf;
    final ResourceLeakTracker<ByteBuf> leak;
}

所以当 ByteBuf 应该被 GC 时,会置入 refQueue。因此对象是否泄露,重点要看 ref.dispose() 方法。

ref.dispose()

其实这里只是判断一下对象是否调用过 track,所以只要进入了 refQueue,则对象一定泄露了。 而我们知道,一个对象一旦失去强引用,就会被 GC 回收。 然而对象从池中来,调用release后会回到池中,也就是说正常使用并回收并不会失去强引用。 而如果一个对象没有正常放回池中,而强引用又丢失了,那么肯定泄露了,泄露者就是最后一次 touch 的位置。

public class ResourceLeakDetector<T> {
    private static final class DefaultResourceLeak<T>
            extends WeakReference<Object> implements ResourceLeakTracker<T>, ResourceLeak {
        private final Set<DefaultResourceLeak<?>> allLeaks;
        boolean dispose() {
            clear();
            return allLeaks.remove(this);
        }
    }
}

记录潜在泄露者

public class ResourceLeakDetector<T> {
    private static final class DefaultResourceLeak<T>
            extends WeakReference<Object> implements ResourceLeakTracker<T>, ResourceLeak {
        @Override
        public String toString() {
            Record oldHead = headUpdater.getAndSet(this, null);
            if (oldHead == null) {
                return EMPTY_STRING;
            }
            StringBuilder buf = new StringBuilder(present * 2048).append(NEWLINE);
            buf.append("Recent access records: ").append(NEWLINE);
            int i = 1;
            Set<String> seen = new HashSet<String>(present);
            for (; oldHead != Record.BOTTOM; oldHead = oldHead.next) {
                String s = oldHead.toString();
                if (seen.add(s)) {
                    if (oldHead.next == Record.BOTTOM) {
                        buf.append("Created at:").append(NEWLINE).append(s);
                    } else {
                        buf.append('#').append(i++).append(':').append(NEWLINE).append(s);
                    }
                } else {
                    duped++;
                }
            }
            //...........
            return buf.toString();
        }
    }
}

Clone this wiki locally