资源泄漏检测器

2019-11-08  本文已影响0人  shallowinggg

虽然java相比c++等语言提供了gc机制,并且屏蔽了指针的概念,但是某些资源依然需要程序员手动释放(比如文件流,数据库连接等),因此由于编码上的疏忽等原因,经常出现资源泄漏的问题。针对此种情况,jdk在1.7版本提供了try-with-resource,实现了AutoCloseable接口的类可以使用此语法自动释放资源,无需在finally块中手动释放。

但是,很多类可能出于各种原因不愿实现此接口,JDK7之前的版本也无法使用try-with-resource,因此需要一种合理的手段来检测资源是否泄漏。

如何检测

JDK自1.2版本提供了Reference类及其子类StrongReference,SoftReference,WeakReference,PhantomReference。其中WeakReference以及PhantomReference并不会对引用对象本身产生影响,即使用它们引用对象时,不会影响JVM进行GC时的可达性分析。因此,可以使用它们对资源进行跟踪,当资源对应的对象被gc时,WeakReferencePhantomReference会被enqueue到某个引用队列中。利用这一个特性,我们可以实现对资源是否调用其release()方法的检测。

检测的开销

当对资源进行泄漏检测时,这无疑会带来一定的开销。因此可以选择对部分资源进行跟踪,当进行测试时,可以选择跟踪全部资源,以保证稳定安全。

code

核心代码如下:

package com.shallowinggg.util;

/**
 * @author shallowinggg
 */
public interface ResourceTracker<T> {

    /**
     * 结束对资源的跟踪。
     * 当调用资源的销毁方法时,调用此方法。
     *
     * @param obj 跟踪对象
     * @return {@literal true} 如果第一次被调用
     */
    boolean close(T obj);
}

package com.shallowinggg.util;

import com.shallowinggg.util.reflect.MethodUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;

/**
 * 资源跟踪器
 *
 * @author shallowinggg
 */
public class ResourceLeakDetector<T> {
    private static final Logger LOGGER = LoggerFactory.getLogger(ResourceLeakDetector.class);

    private static final String PROP_SAMPLE_RATIO = "leakDetector.sampleRatio";
    private static final int DEFAULT_SAMPLE_RATIO = 128;
    private static final int SAMPLE_RATIO;

    private static final String PROP_LEVEL = "leakDetector.level";
    private static final Level DEFAULT_LEVEL = Level.SIMPLE;
    private static final Level LEVEL;

    /**
     * 所有跟踪器
     * 当对某个对象进行跟踪时,注册跟踪器。
     */
    private Set<ResourceTracker<T>> trackers = Collections.newSetFromMap(new ConcurrentHashMap<>());

    /**
     * 对象引用队列
     * 提供给跟踪器使用,跟踪器继承{@link WeakReference}。
     */
    private ReferenceQueue<T> referenceQueue = new ReferenceQueue<>();

    /**
     * 资源类名称
     */
    private String resourceType;

    /**
     * 跟踪样本比例
     * 为了减少开销,不对所有对象实例进行跟踪,只随机跟踪部分实例。
     * 随机跟踪方式为 {@code random.nextInt(sampleRatio) == 0},默认为128,即跟踪1%的实例。
     * 可以通过构造方法指定或者设置系统属性{@literal leakDetector.sampleRatio}。
     */
    private final int sampleRatio;

    private static Level level;

    public ResourceLeakDetector(String resourceType) {
        this(resourceType, SAMPLE_RATIO);
    }

    public ResourceLeakDetector(Class<?> resourceType) {
        this(resourceType.getName(), SAMPLE_RATIO);
    }

    public ResourceLeakDetector(String resourceType, int sampleRatio) {
        this.resourceType = resourceType;
        this.sampleRatio = sampleRatio;
    }


    public ResourceTracker<T> track(T obj) {
        Level level = ResourceLeakDetector.level;
        if(Level.DISABLE == level) {
            return null;
        }
        if(Level.SIMPLE == level) {
            if (ThreadLocalRandom.current().nextInt(sampleRatio) == 0) {
                reportLeak();
                return new DefaultResourceTracker<>(obj, referenceQueue, trackers, null);
            }
            return null;
        }

        String caller = MethodUtil.getCaller();
        reportLeak();
        return new DefaultResourceTracker<>(obj, referenceQueue, trackers, caller);
    }

    private void reportLeak() {
        for(;;) {
            @SuppressWarnings("unchecked")
            DefaultResourceTracker<T> tracker = (DefaultResourceTracker<T>) referenceQueue.poll();
            if(tracker == null) {
                break;
            }

            if(!tracker.dispose()) {
                continue;
            }

            if(tracker.getCallSite() == null) {
                LOGGER.error("LEAK: {}.release() was not called before it's garbage-collected. ", resourceType);
            } else {
                LOGGER.error("LEAK: {}.release() was not called before it's garbage-collected. CallSite: {}"
                        , resourceType, tracker.getCallSite());
            }
        }
    }


    private static class DefaultResourceTracker<T> extends WeakReference<T> implements ResourceTracker<T> {
        private int hash;
        private Set<ResourceTracker<T>> trackers;
        private String callSite;

        DefaultResourceTracker(T obj, ReferenceQueue<T> queue, Set<ResourceTracker<T>> trackers, String callSite) {
            super(obj, queue);
            assert obj != null;
            this.hash = System.identityHashCode(obj);
            this.callSite = callSite;
            trackers.add(this);
            this.trackers = trackers;
        }

        boolean dispose() {
            clear();
            return trackers.remove(this);
        }

        @Override
        public boolean close(T obj) {
            assert hash == System.identityHashCode(obj);
            try {
                if (trackers.remove(this)) {
                    clear();
                    return true;
                }
                return false;
            } finally {
                // 需要在调用Reference#clear()后保证对obj的可达性。
                // 因为JIT / GC 可能在执行完System.identityHashCode(obj)后
                // 判定obj实例不再使用,于是将其回收并加入到ReferenceQueue中,
                // 如果此时有其他线程在调用track()方法,这将会导致误报。
                // https://stackoverflow.com/questions/26642153/finalize-called-on-strongly-reachable-objects-in-java-8#
                reachabilityFence0(obj);
            }
        }

        /**
         * Java9 提供了Reference#reachabilityFence(Object)方法,可以用来代替此方法。
         * https://docs.oracle.com/javase/9/docs/api/java/lang/ref/Reference.html#reachabilityFence-java.lang.Object-
         *
         * @param ref 引用对象
         */
        private static void reachabilityFence0(Object ref) {
            if(ref != null) {
                synchronized (ref) {
                    // 编译器不会将空synchronized块优化掉
                }
            }
        }

        public String getCallSite() {
            return callSite;
        }

        @Override
        public int hashCode() {
            return super.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            return super.equals(obj);
        }
    }

    public enum Level {
        /**
         * 禁用
         */
        DISABLE,
        /**
         * 进行简单的抽样跟踪
         */
        SIMPLE,
        /**
         * 对全部对象进行跟踪
         */
        PARANOID;

        public static Level parse(String val) {
            val = val.trim();
            for(Level level : values()) {
                if(level.name().equals(val.toUpperCase()) || val.equals(String.valueOf(level.ordinal()))) {
                    return level;
                }
            }
            return DEFAULT_LEVEL;
        }
    }


    static {
        String level = SystemPropertyUtil.get(PROP_LEVEL);
        LEVEL = Level.parse(level);
        ResourceLeakDetector.level = LEVEL;
        SAMPLE_RATIO = SystemPropertyUtil.getInt(PROP_SAMPLE_RATIO, DEFAULT_SAMPLE_RATIO);

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("-D{}: {}", PROP_SAMPLE_RATIO, SAMPLE_RATIO);
            LOGGER.debug("-D{}: {}", PROP_LEVEL, LEVEL);
        }
    }

}

为了方便开发,引用了另外两个工具类:

package com.shallowinggg.util.reflect;

/**
 * @author shallowinggg
 */
public class MethodUtil {
    /**
     * 栈轨迹只有三层时,当前方法已是最高调用者
     */
    private static final int TOP_STACK_INDEX = 3;

    private MethodUtil() {}

    public static String getCaller() {
        StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
        StackTraceElement prevStackTrace;
        if(stackTraceElements.length == TOP_STACK_INDEX) {
            prevStackTrace = stackTraceElements[2];
        } else {
            prevStackTrace = stackTraceElements[3];
        }
        return prevStackTrace.getClassName() + "." + prevStackTrace.getMethodName();
    }

}
package com.shallowinggg.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.security.AccessController;
import java.security.PrivilegedAction;

/**
 * A collection of utility methods to retrieve and parse the values of the Java system properties.
 */
public final class SystemPropertyUtil {

    private static final Logger logger = LoggerFactory.getLogger(SystemPropertyUtil.class);

    /**
     * Returns {@code true} if and only if the system property with the specified {@code key}
     * exists.
     */
    public static boolean contains(String key) {
        return get(key) != null;
    }

    /**
     * Returns the value of the Java system property with the specified
     * {@code key}, while falling back to {@code null} if the property access fails.
     *
     * @return the property value or {@code null}
     */
    public static String get(String key) {
        return get(key, null);
    }

    /**
     * Returns the value of the Java system property with the specified
     * {@code key}, while falling back to the specified default value if
     * the property access fails.
     *
     * @return the property value.
     *         {@code def} if there's no such property or if an access to the
     *         specified property is not allowed.
     */
    public static String get(final String key, String def) {
        if (key == null) {
            throw new NullPointerException("key");
        }
        if (key.isEmpty()) {
            throw new IllegalArgumentException("key must not be empty.");
        }

        String value = null;
        try {
            if (System.getSecurityManager() == null) {
                value = System.getProperty(key);
            } else {
                value = AccessController.doPrivileged((PrivilegedAction<String>) () -> System.getProperty(key));
            }
        } catch (SecurityException e) {
            logger.warn("Unable to retrieve a system property '{}'; default values will be used.", key, e);
        }

        if (value == null) {
            return def;
        }

        return value;
    }

    /**
     * Returns the value of the Java system property with the specified
     * {@code key}, while falling back to the specified default value if
     * the property access fails.
     *
     * @return the property value.
     *         {@code def} if there's no such property or if an access to the
     *         specified property is not allowed.
     */
    public static boolean getBoolean(String key, boolean def) {
        String value = get(key);
        if (value == null) {
            return def;
        }

        value = value.trim().toLowerCase();
        if (value.isEmpty()) {
            return def;
        }

        if ("true".equals(value) || "yes".equals(value) || "1".equals(value)) {
            return true;
        }

        if ("false".equals(value) || "no".equals(value) || "0".equals(value)) {
            return false;
        }

        logger.warn(
                "Unable to parse the boolean system property '{}':{} - using the default value: {}",
                key, value, def
        );

        return def;
    }

    /**
     * Returns the value of the Java system property with the specified
     * {@code key}, while falling back to the specified default value if
     * the property access fails.
     *
     * @return the property value.
     *         {@code def} if there's no such property or if an access to the
     *         specified property is not allowed.
     */
    public static int getInt(String key, int def) {
        String value = get(key);
        if (value == null) {
            return def;
        }

        value = value.trim();
        try {
            return Integer.parseInt(value);
        } catch (Exception e) {
            // Ignore
        }

        logger.warn(
                "Unable to parse the integer system property '{}':{} - using the default value: {}",
                key, value, def
        );

        return def;
    }

    /**
     * Returns the value of the Java system property with the specified
     * {@code key}, while falling back to the specified default value if
     * the property access fails.
     *
     * @return the property value.
     *         {@code def} if there's no such property or if an access to the
     *         specified property is not allowed.
     */
    public static long getLong(String key, long def) {
        String value = get(key);
        if (value == null) {
            return def;
        }

        value = value.trim();
        try {
            return Long.parseLong(value);
        } catch (Exception e) {
            // Ignore
        }

        logger.warn(
                "Unable to parse the long integer system property '{}':{} - using the default value: {}",
                key, value, def
        );

        return def;
    }

    private SystemPropertyUtil() {
        // Unused
    }
}

test

package com.shallowinggg;

import com.shallowinggg.util.ResourceLeakDetector;
import com.shallowinggg.util.ResourceTracker;
import org.junit.Test;

public class ResourceTrackerTest {
    private static ResourceLeakDetector<Resource> detector = new ResourceLeakDetector<>(Resource.class);

    @Test
    public void testUnRelease() {
        // -DleakDetector.level=2
        Resource resource = new AdvancedResource();
        resource = null;
        for(int i = 0; i < 1_000_000_000; ++i) {
            if(i % 1_000_0000 == 0) {
                System.gc();
            }
        }
        ResourceTracker<Resource> newTracker = detector.track(new AdvancedResource());
        synchronized (newTracker) {
        }
    }

    @Test
    public void testRelease() {
        // -DleakDetector.level=2
        Resource resource = new AdvancedResource();
        resource.release();
        for(int i = 0; i < 1_000_000_000; ++i) {
            if(i % 1_000_0000 == 0) {
                System.gc();
            }
        }
        ResourceTracker<Resource> newTracker = detector.track(new AdvancedResource());
        synchronized (newTracker) {
        }
    }

    private static class Resource {

        public void release() {
            System.out.println("close resource");
        }
    }

    private static class AdvancedResource extends Resource {
        private ResourceTracker<Resource> tracker;

        AdvancedResource() {
            this.tracker = detector.track(this);
        }

        @Override
        public void release() {
            super.release();
            tracker.close(this);
        }
    }
}

测试结果:

2019-11-06 22:09:45,915 [com.shallowinggg.util.ResourceLeakDetector.<clinit>(ResourceLeakDetector.java:211)]-[DEBUG] -DleakDetector.sampleRatio: 128
2019-11-06 22:09:45,918 [com.shallowinggg.util.ResourceLeakDetector.<clinit>(ResourceLeakDetector.java:212)]-[DEBUG] -DleakDetector.level: PARANOID
2019-11-06 22:09:47,634 [com.shallowinggg.util.ResourceLeakDetector.reportLeak(ResourceLeakDetector.java:104)]-[ERROR] LEAK: com.shallowinggg.ResourceTrackerTest$Resource.release() was not called before it's garbage-collected. CallSite:com.shallowinggg.ResourceTrackerTest$AdvancedResource.<init>

2019-11-06 22:14:15,736 [com.shallowinggg.util.ResourceLeakDetector.<clinit>(ResourceLeakDetector.java:211)]-[DEBUG] -DleakDetector.sampleRatio: 128
2019-11-06 22:14:15,738 [com.shallowinggg.util.ResourceLeakDetector.<clinit>(ResourceLeakDetector.java:212)]-[DEBUG] -DleakDetector.level: PARANOID
close resource

注意点

  1. 关于资源跟踪,选择WeakReference还是PhantomReference都可以。
  2. 关于跟踪的准确度,此处只提供了跟踪器构造的函数调用点,如果需要更精细化的控制,可以定制相应的需要。



参考资料: Netty

上一篇下一篇

猜你喜欢

热点阅读