雖然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í)疲陕,WeakReference
和PhantomReference
會被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)
- 關(guān)于資源跟蹤秧荆,選擇
WeakReference
還是PhantomReference
都可以倔毙。 - 關(guān)于跟蹤的準(zhǔn)確度,此處只提供了跟蹤器構(gòu)造的函數(shù)調(diào)用點(diǎn)乙濒,如果需要更精細(xì)化的控制陕赃,可以定制相應(yīng)的需要。
參考資料: Netty