1、作用
用于内存申请后的内存对象检测泄露; 为什么会有这个?
- Netty为了防止数据之间的来回拷贝,提升数据传输效率,故默认使用堆外内存;(因为堆内内存的数据传递到堆外,也需要一次数据拷贝)
- Netty申请的堆外内存,需要额外注意堆外内存的主动管理(申请、重利用、释放等);如果申请没有释放,就可能会有内存泄露的风险;
2、使用方式
调用API PooledByteBufAllocator.newDirectBuffer 或者是PooledByteBufAllocator.newHeapBuffer 便可自动触发LeakDetector检测
protected static ByteBuf toLeakAwareBuffer(ByteBuf buf) {
ResourceLeakTracker<ByteBuf> leak;
switch (ResourceLeakDetector.getLevel()) {
case SIMPLE:
leak = AbstractByteBuf.leakDetector.track(buf);
if (leak != null) {
buf = new SimpleLeakAwareByteBuf(buf, leak);
}
break;
case ADVANCED:
case PARANOID:
leak = AbstractByteBuf.leakDetector.track(buf);
if (leak != null) {
buf = new AdvancedLeakAwareByteBuf(buf, leak);
}
break;
default:
break;
}
return buf;
}
3、原理
一句话概括:在申请内存时,进行检测是否有内存引用的没有被释放,如果有,则说明内存有泄漏;
- 通过调用newDirectBufferAPI时,利用Delegate模式进行封装一层包装成LeakAwareByteBuf
- 当创建LeakAwareByteBuf对象时,做一次检测,将ResourceLeakDetector中的所有DefaultResourceLeak进行遍历,查看head == null; 如果为空,则已经close;如果不为空,则说明有内存泄露;
- 注意:每次创建对象的时候,才会做一次内存检测;
3.1 关键技术点-虚引用
可以参看我的另一篇文章 -- Java中弱引用在开源框架如何使用?
3.3 整体类图
classDiagram
AbstractByteBuf <|-- PooledDirectedByteBuf :继承
PooledDirectedByteBuf --> LeakAwareByteBuf
AbstractByteBuf --> ResourceLeakDetector
ResourceLeakDetector o-- DefaultResourceLeak
ResourceLeakTracker <|-- DefaultResourceLeak
WeakReference<|-- DefaultResourceLeak
class AbstractByteBuf {
+ResourceLeakDetector detector
}
class ResourceLeakDetector{
-Set<DefaultResourceLeak> allLeaks
-ReferenceQueue refQueues
-ResourceLeak level
+reportLeak()
-track0()
}
class DefaultResourceLeak{
-Record head
-AtomicReferenceFieldUpdater headUpdater
-Set<DefaultLeakResource> allLeaks
}
class ResourceLeakTracker{
+record()
}
3.4 整体流程图
4、源码解析
4.1 如何检测内存泄露
入口:toLeakAwareBuffer
检测级别
- SIMPLE : 简单信息,而且是抽样获取使用该内存对象的位置;
- ADVANCED:提供更详细的上下文信息,跟踪资源的整个生命周期轨迹;
- PARANOID:仅限调试使用,该方式会对资源的每一种使用方式都会进行跟踪、记录,内容非常详细;但也是最耗时
protected static ByteBuf toLeakAwareBuffer(ByteBuf buf) {
ResourceLeakTracker<ByteBuf> leak;
// 根据检测级别进行判断封装哪一种Delegator
switch (ResourceLeakDetector.getLevel()) {
case SIMPLE:
// 检测buffer使用
leak = AbstractByteBuf.leakDetector.track(buf);
if (leak != null) {
buf = new SimpleLeakAwareByteBuf(buf, leak);
}
break;
case ADVANCED:
case PARANOID:
leak = AbstractByteBuf.leakDetector.track(buf);
if (leak != null) {
buf = new AdvancedLeakAwareByteBuf(buf, leak);
}
break;
default:
break;
}
return buf;
}
采样跟踪 LeakResourceDetector.track
private DefaultResourceLeak track0(T obj) {
Level level = ResourceLeakDetector.level;
if (level == Level.DISABLED) {
return null;
}
// 如果检测等级小于PARANOID,就会随机采样进行定期检测;
if (level.ordinal() < Level.PARANOID.ordinal()) {
// 默认samplingInterval = 128, 也就是1/128的概率进行检测;
if ((PlatformDependent.threadLocalRandom().nextInt(samplingInterval)) == 0) {
reportLeak();
return new DefaultResourceLeak(obj, refQueue, allLeaks);
}
return null;
}
// 如果检测等级=PARANOID, 直接检测;
reportLeak();
return new DefaultResourceLeak(obj, refQueue, allLeaks);
}
弱引用判断 LeakResourceDetector.reportLeak
private void reportLeak() {
// 如果没有开启error级别,就不会打印;基本走不进去这个判断;
if (!logger.isErrorEnabled()) {
clearRefQueue();
return;
}
// Detect and report previous leaks.
for (;;) {
@SuppressWarnings("unchecked")
// 从弱引用队列里面获取元素,一旦有元素,说明这个对象已经被GC掉,这里做一个检测用于后置清理;
// 这种黑科技方式使用的还比较多,普遍用于资源的后置清理
DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
if (ref == null) {
break;
}
if (!ref.dispose()) {
continue;
}
// 调用DefaultResourceLeak.toString()
String records = ref.toString();
if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) {
if (records.isEmpty()) {
reportUntracedLeak(resourceType);
} else {
reportTracedLeak(resourceType, records);
}
}
}
}
输出所有调用点堆栈:DefaultResourceLeak.toString
public String toString() {
Record oldHead = headUpdater.getAndSet(this, null);
// 如果headUpdater指向的元素是null,则说明已经关闭了;直接返回即可;
if (oldHead == null) {
// Already closed
return EMPTY_STRING;
}
final int dropped = droppedRecordsUpdater.get(this);
int duped = 0;
int present = oldHead.pos + 1;
// Guess about 2 kilobytes per stack trace
StringBuilder buf = new StringBuilder(present * 2048).append(NEWLINE);
buf.append("Recent access records: ").append(NEWLINE);
int i = 1;
Set<String> seen = new HashSet<String>(present);
// 遍历当前链表,如果是链尾,就把Created at字符拼上,说明是创建的地方;
// 如果不是链尾,则正常将序号拼上,说明有多少处使用的地方;
// 这里也涉及到Record.toString
for (; oldHead != Record.BOTTOM; oldHead = oldHead.next) {
String s = oldHead.toString();
if (seen.add(s)) {
if (oldHead.next == Record.BOTTOM) {
buf.append("Created at:").append(NEWLINE).append(s);
} else {
buf.append('#').append(i++).append(':').append(NEWLINE).append(s);
}
} else {
duped++;
}
}
if (duped > 0) {
buf.append(": ")
.append(duped)
.append(" leak records were discarded because they were duplicates")
.append(NEWLINE);
}
if (dropped > 0) {
buf.append(": ")
.append(dropped)
.append(" leak records were discarded because the leak record count is targeted to ")
.append(TARGET_RECORDS)
.append(". Use system property ")
.append(PROP_TARGET_RECORDS)
.append(" to increase the limit.")
.append(NEWLINE);
}
buf.setLength(buf.length() - NEWLINE.length());
return buf.toString();
}
打印详细堆栈:Record.toString
实现很简单;Record继承Throwable,直接调用getStackTrace 方法就可以获取当前的堆栈信息;
这里就不贴代码了;
4.2 如何记录内存泄漏位置
以AdvancedLeakAwareBytebuf为例,以 readByte为例; AdvancedLeakAwareBytebuf就是一个Delegator,用于添加、丰富额外逻辑
入口 AdvancedLeakAwareBytebuf.readByte()
public byte readByte() {
recordLeakNonRefCountingOperation(leak);
return super.readByte();
}
内存记录:AdvancedLeakAwareByteBuf.recordLeakNonRefCountingOperation
static void recordLeakNonRefCountingOperation(ResourceLeakTracker<ByteBuf> leak) {
if (!ACQUIRE_AND_RELEASE_ONLY) {
leak.record();
}
}
DefaultResourceLeak.record
private void record0(Object hint) {
// Check TARGET_RECORDS > 0 here to avoid similar check before remove from and add to lastRecords
if (TARGET_RECORDS > 0) {
Record oldHead;
Record prevHead;
Record newHead;
boolean dropped;
do {
// 如果已经null,说明已经关闭了;
if ((prevHead = oldHead = headUpdater.get(this)) == null) {
// already closed.
return;
}
final int numElements = oldHead.pos + 1;
// TARGET_RECORDS 默认是4;如果当前添加的Record>=4,就会采取抽样进行丢弃;
// 因为采样太多,就会导致占用内存过多;浪费空间;
if (numElements >= TARGET_RECORDS) {
// 举例:若numElements=8, 就会从(0,1<<4)区间内选择一个数字,判断是否=0,若等于0,则说明需要丢弃;丢弃的信息会在打印内存泄露的信息里面显示出来的;
final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30);
if (dropped = PlatformDependent.threadLocalRandom().nextInt(1 << backOffFactor) != 0) {
prevHead = oldHead.next;
}
} else {
dropped = false;
}
newHead = hint != null ? new Record(prevHead, hint) : new Record(prevHead);
} while (!headUpdater.compareAndSet(this, oldHead, newHead));
if (dropped) {
droppedRecordsUpdater.incrementAndGet(this);
}
}
}
内存释放 ByteBuf.release
public boolean release() {
if (super.release()) {
// 把对应的DefaultResourceLeak关闭掉;如何关闭?见下文
closeLeak();
return true;
}
return false;
}
DefaultResourceLeak.close 将headUpdater的head设置为null,代表已经释放,前文中在track的时候,就是根据head来进行判断;
public boolean close() {
if (allLeaks.remove(this)) {
// Call clear so the reference is not even enqueued.
clear();
headUpdater.set(this, null);
return true;
}
return false;
}
5、学习心得
学有所获,方能不负韶华
5.1 什么时间点进行内存检测?
1、定时监测? 显然太low
2、在GC以后,垃圾回收的时候进行检测;可是这个时间点,垃圾对象已经被回收掉了,怎么才能判断呢?
3、弱、软、虚引用就排上用场了;
这里面DefaultResourceLeak就是一个弱引用,在里面维护了一个内存泄漏点链表(每个泄漏点都有描述)
- 当强引用对象被GC后,相对应的ByteBuf对象也会被销毁掉,但是这个ByteBuf对象所引用的堆外内存因为没有release释放,所以这个时候就会出现内存泄露,故DefaultResourceLeak的检测机制就出现了,每次GC后,在refQueue中就会出现对应的DefaultResourceLeak,然后判断其headUpdater是否为null即可,很精巧;
5.2 委托模式
利用LeakAwakeByteBuf做一层包装,来增加额外、附加的功能;