Netty源码分析——no cleaner策略

Netty源码分析——no cleaner策略

DirectByteBuffer中的Cleaner

要说Netty的noCleaner策略,还是要先看看底层DirectByteBuffer中的Cleaner。由于我们使用了堆外内存,所以我们要管理这些堆外内存,这里JDK底层提供了Cleaner来作为释放的方式之一。我们看看直接内存的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private final Cleaner cleaner;

public Cleaner cleaner() { return cleaner; }

DirectByteBuffer(int cap) {
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
// 标记分配了多少内存,用来控制使用的堆外内存量
Bits.reserveMemory(size, cap);
long base = 0;
try {
// 尝试分配内存
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 创建Cleaner,这里Deallocator是一个runnable
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}

这里我们会在构造函数里创建一个Cleaner,看下create方法及相关方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 这里的var0就是DirectByteBuffer
public static Cleaner create(Object var0, Runnable var1) {
return var1 == null ? null : add(new Cleaner(var0, var1));
}

private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();

// cleaner本身是一个PhantomReference
// 这里var1就是DirectByteBuffer,var2是Deallocator这个runnable
private Cleaner(Object var1, Runnable var2) {
super(var1, dummyQueue);
this.thunk = var2;
}

// 构建一个双向链表
// var0是新的cleaner实例
private static synchronized Cleaner add(Cleaner var0) {
if (first != null) {
var0.next = first;
first.prev = var0;
}
first = var0;
return var0;
}

这里我们就做了两件事:

  1. 把我们分配的DirectByteBuffer和一个PhantomReference关联起来
  2. cleaner构建成一个双向链表

我们看看Cleaner中有一个clean方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void clean() {
// 把自己从链表中移除
if (remove(this)) {
try {
// thunk就是我们上面说到的Deallocator
this.thunk.run();
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}

System.exit(1);
return null;
}
});
}
}
}

再来看看Deallocator这个类的run方法:

1
2
3
4
5
6
7
8
9
public void run() {
if (address == 0) {
return;
}
// 调用free释放内存
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}

好了,现在我们知道Cleaner是做什么的了,它本质上是一个幻影引用,执行clean的时候会执行我们设置进去的Runnable对象。对于DirectByteBuffer来说,设置进去的Runnable实例的作用就是释放内存。

那么什么时候我们的Cleaner可以执行clean方法呢?答案是在ReferenceHandler这个线程里,Reference这给类在加载的时候,会启动ReferenceHandler这个线程,看看这个线程的run方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
for (;;) {
Reference<Object> r;
synchronized (lock) {
if (pending != null) {
r = pending;
pending = r.discovered;
r.discovered = null;
} else {
try {
try {
lock.wait();
} catch (OutOfMemoryError x) { }
} catch (InterruptedException x) { }
continue;
}
}
if (r instanceof Cleaner) {
((Cleaner)r).clean();
continue;
}
ReferenceQueue<Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
}

这里要提一下ReferenceReferenceQueue的工作原理,Reference里有个静态字段pending,当一个Referencereferent被回收时,垃圾回收器会把Reference添加到pending这个链表里,然后ReferenceHandler不断的读取pending中的reference,把它加入到对应的ReferenceQueue中。联合使用的主要作用就是当reference指向的referent回收时,提供一种通知机制,通过queue取到这些reference,来做额外的处理工作。

但是我们可以从上面的代码中发现,如果referent被回收,而Reference恰好又是Cleaner实例,就不会进入ReferenceQueue,而是直接调用clean方法。

所以我们可以总结一下,DirectByteBufferCleaner的关系就是当DirectByteBuffer被回收的时候(这里指的是没有强引用关联这个Buffer),释放我们申请的堆外内存,来保证我们不会产生对外内存泄露。

Netty的no cleaner策略

我们说过了有cleaner的方式,已经觉得设计的非常完善了,保证我们的堆外内存不会泛滥,而且保证了在DirectByteBuffer被回收的时候,堆外内存也可以回收(当然如果内存泄露了是两码事)。

来看看入口,入口在UnpooledByteBufAllocatornewDirectBuffer方法里:

1
2
3
4
5
6
if (PlatformDependent.hasUnsafe()) {
buf = noCleaner ? new InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf(this, initialCapacity, maxCapacity) :
new InstrumentedUnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity);
} else {
buf = new InstrumentedUnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
}

如果可以使用unsafe并且开启noCleaner策略,使用InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf。关于不能使用noCleaner的场景我们就不细说了,实际上是在释放的时候直接使用Cleaner.clean来进行内存释放。释放的方式是通过反射获取clean这个方法或者内部的cleaner这个字段,来执行方法或者获取Cleaner对象后调用clean。可以看下CleanerJava6这个类。

那么说说我们的主角,noCleaner下的DirectByteBuffer,这时候我们创建的对象是InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf的实例,我们关注一下如何释放:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protected void freeDirect(ByteBuffer buffer) {
int capacity = buffer.capacity();
super.freeDirect(buffer);
((UnpooledByteBufAllocator) alloc()).decrementDirect(capacity);
}

protected void freeDirect(ByteBuffer buffer) {
PlatformDependent.freeDirectNoCleaner(buffer);
}

public static void freeDirectNoCleaner(ByteBuffer buffer) {
// 确保使用了no cleaner策略
assert USE_DIRECT_BUFFER_NO_CLEANER;
int capacity = buffer.capacity();
// 调用free memory
PlatformDependent0.freeMemory(PlatformDependent0.directBufferAddress(buffer));
decrementMemoryCounter(capacity);
}

static void freeMemory(long address) {
// 调用unsafe的freememory
UNSAFE.freeMemory(address);
}

这个链路非常简单。总结一下no cleaner策略和has cleaner的区别:

  1. no cleaner尝试获取Cleaner字段或者Cleanerclean方法,执行释放。
  2. has cleaner直接调用unsafe来释放堆外内存。

那么可能有人要问为什么了,为什么还要使用no cleaner策略呢(这是Netty的默认策略)?

我认为原因有二:

  1. Netty实现了引用计数,我们不需要jdk帮助我们进行内存回收。这也是为什么Netty自己实现了泄露检测。由于希望释放由我们自己来操作,所以没有必要使用Cleaner
  2. 性能的一点点提升,在使用no cleaner策略的时候,Netty会通过反射得到DirectByteBuffer的构造函数,不过获取到的构造函数是两个入参的构造函数,非常简单,可以看下private DirectByteBuffer(long addr, int cap)这个方法,不会进行创建Cleaner等措施。

另外,可能Netty认为jdk的BitsreserveMemory设计的也不怎么样(里面有我们经常说到的手动调用System.gc的问题),所以就放弃了使用原生的Cleaner策略。