美文网首页
资源泄漏检测器

资源泄漏检测器

作者: shallowinggg | 来源:发表于2019-11-08 14:49 被阅读0次

    虽然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

    相关文章

      网友评论

          本文标题:资源泄漏检测器

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