资源泄漏检测器
虽然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时,WeakReference
和PhantomReference
会被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
注意点
- 关于资源跟踪,选择
WeakReference
还是PhantomReference
都可以。 - 关于跟踪的准确度,此处只提供了跟踪器构造的函数调用点,如果需要更精细化的控制,可以定制相应的需要。
参考资料: Netty