資源泄漏檢測器

雖然java相比c++等語言提供了gc機(jī)制棒厘,并且屏蔽了指針的概念丈秩,但是某些資源依然需要程序員手動(dòng)釋放(比如文件流,數(shù)據(jù)庫連接等)一也,因此由于編碼上的疏忽等原因,經(jīng)常出現(xiàn)資源泄漏的問題喉脖。針對此種情況塘秦,jdk在1.7版本提供了try-with-resource,實(shí)現(xiàn)了AutoCloseable接口的類可以使用此語法自動(dòng)釋放資源动看,無需在finally塊中手動(dòng)釋放尊剔。

但是,很多類可能出于各種原因不愿實(shí)現(xiàn)此接口菱皆,JDK7之前的版本也無法使用try-with-resource须误,因此需要一種合理的手段來檢測資源是否泄漏。

如何檢測

JDK自1.2版本提供了Reference類及其子類StrongReference,SoftReference,WeakReference,PhantomReference仇轻。其中WeakReference以及PhantomReference并不會對引用對象本身產(chǎn)生影響京痢,即使用它們引用對象時(shí),不會影響JVM進(jìn)行GC時(shí)的可達(dá)性分析篷店。因此祭椰,可以使用它們對資源進(jìn)行跟蹤,當(dāng)資源對應(yīng)的對象被gc時(shí)疲陕,WeakReferencePhantomReference會被enqueue到某個(gè)引用隊(duì)列中方淤。利用這一個(gè)特性,我們可以實(shí)現(xiàn)對資源是否調(diào)用其release()方法的檢測蹄殃。

檢測的開銷

當(dāng)對資源進(jìn)行泄漏檢測時(shí)携茂,這無疑會帶來一定的開銷。因此可以選擇對部分資源進(jìn)行跟蹤诅岩,當(dāng)進(jìn)行測試時(shí)讳苦,可以選擇跟蹤全部資源带膜,以保證穩(wěn)定安全。

code

核心代碼如下:

package com.shallowinggg.util;

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

    /**
     * 結(jié)束對資源的跟蹤鸳谜。
     * 當(dāng)調(diào)用資源的銷毀方法時(shí)膝藕,調(diào)用此方法。
     *
     * @param obj 跟蹤對象
     * @return {@literal true} 如果第一次被調(diào)用
     */
    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;

    /**
     * 所有跟蹤器
     * 當(dāng)對某個(gè)對象進(jìn)行跟蹤時(shí)咐扭,注冊跟蹤器芭挽。
     */
    private Set<ResourceTracker<T>> trackers = Collections.newSetFromMap(new ConcurrentHashMap<>());

    /**
     * 對象引用隊(duì)列
     * 提供給跟蹤器使用,跟蹤器繼承{@link WeakReference}草描。
     */
    private ReferenceQueue<T> referenceQueue = new ReferenceQueue<>();

    /**
     * 資源類名稱
     */
    private String resourceType;

    /**
     * 跟蹤樣本比例
     * 為了減少開銷览绿,不對所有對象實(shí)例進(jìn)行跟蹤策严,只隨機(jī)跟蹤部分實(shí)例穗慕。
     * 隨機(jī)跟蹤方式為 {@code random.nextInt(sampleRatio) == 0},默認(rèn)為128妻导,即跟蹤1%的實(shí)例逛绵。
     * 可以通過構(gòu)造方法指定或者設(shè)置系統(tǒng)屬性{@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 {
                // 需要在調(diào)用Reference#clear()后保證對obj的可達(dá)性倔韭。
                // 因?yàn)镴IT / GC 可能在執(zhí)行完System.identityHashCode(obj)后
                // 判定obj實(shí)例不再使用术浪,于是將其回收并加入到ReferenceQueue中,
                // 如果此時(shí)有其他線程在調(diào)用track()方法寿酌,這將會導(dǎo)致誤報(bào)胰苏。
                // 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塊優(yōu)化掉
                }
            }
        }

        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,
        /**
         * 進(jìn)行簡單的抽樣跟蹤
         */
        SIMPLE,
        /**
         * 對全部對象進(jìn)行跟蹤
         */
        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);
        }
    }

}

為了方便開發(fā)硕并,引用了另外兩個(gè)工具類:

package com.shallowinggg.util.reflect;

/**
 * @author shallowinggg
 */
public class MethodUtil {
    /**
     * 棧軌跡只有三層時(shí),當(dāng)前方法已是最高調(diào)用者
     */
    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);
        }
    }
}

測試結(jié)果:

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

注意點(diǎn)

  1. 關(guān)于資源跟蹤秧荆,選擇WeakReference還是PhantomReference都可以倔毙。
  2. 關(guān)于跟蹤的準(zhǔn)確度,此處只提供了跟蹤器構(gòu)造的函數(shù)調(diào)用點(diǎn)乙濒,如果需要更精細(xì)化的控制陕赃,可以定制相應(yīng)的需要。



參考資料: Netty

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末颁股,一起剝皮案震驚了整個(gè)濱河市么库,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌甘有,老刑警劉巖廊散,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異梧疲,居然都是意外死亡允睹,警方通過查閱死者的電腦和手機(jī)运准,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缭受,“玉大人胁澳,你說我怎么就攤上這事∶渍撸” “怎么了韭畸?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蔓搞。 經(jīng)常有香客問我胰丁,道長,這世上最難降的妖魔是什么喂分? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任锦庸,我火速辦了婚禮,結(jié)果婚禮上蒲祈,老公的妹妹穿的比我還像新娘甘萧。我一直安慰自己,他們只是感情好梆掸,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布扬卷。 她就那樣靜靜地躺著,像睡著了一般酸钦。 火紅的嫁衣襯著肌膚如雪怪得。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天卑硫,我揣著相機(jī)與錄音徒恋,去河邊找鬼。 笑死拔恰,一個(gè)胖子當(dāng)著我的面吹牛因谎,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播颜懊,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼财岔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了河爹?” 一聲冷哼從身側(cè)響起匠璧,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎咸这,沒想到半個(gè)月后夷恍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡媳维,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年酿雪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了遏暴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡指黎,死狀恐怖朋凉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情醋安,我是刑警寧澤杂彭,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站吓揪,受9級特大地震影響亲怠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜柠辞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一团秽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧钾腺,春花似錦徙垫、人聲如沸讥裤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽己英。三九已至间螟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間损肛,已是汗流浹背厢破。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留治拿,地道東北人摩泪。 一個(gè)月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像劫谅,于是被迫代替她去往敵國和親见坑。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355