Netty源码分析——no cleaner策略
DirectByteBuffer中的Cleaner
要说Netty的noCleaner策略,还是要先看看底层DirectByteBuffer
中的Cleaner
。由于我们使用了堆外内存,所以我们要管理这些堆外内存,这里JDK底层提供了Cleaner
来作为释放的方式之一。我们看看直接内存的构造函数:
1 | private final Cleaner cleaner; |
这里我们会在构造函数里创建一个Cleaner
,看下create
方法及相关方法:
1 | // 这里的var0就是DirectByteBuffer |
这里我们就做了两件事:
- 把我们分配的
DirectByteBuffer
和一个PhantomReference
关联起来 - 把
cleaner
构建成一个双向链表
我们看看Cleaner
中有一个clean
方法:
1 | public void clean() { |
再来看看Deallocator
这个类的run
方法:
1 | public void run() { |
好了,现在我们知道Cleaner
是做什么的了,它本质上是一个幻影引用,执行clean
的时候会执行我们设置进去的Runnable
对象。对于DirectByteBuffer
来说,设置进去的Runnable
实例的作用就是释放内存。
那么什么时候我们的Cleaner
可以执行clean
方法呢?答案是在ReferenceHandler
这个线程里,Reference
这给类在加载的时候,会启动ReferenceHandler
这个线程,看看这个线程的run
方法:
1 | for (;;) { |
这里要提一下Reference
和ReferenceQueue
的工作原理,Reference
里有个静态字段pending
,当一个Reference
的referent
被回收时,垃圾回收器会把Reference
添加到pending
这个链表里,然后ReferenceHandler
不断的读取pending
中的reference
,把它加入到对应的ReferenceQueue
中。联合使用的主要作用就是当reference
指向的referent
回收时,提供一种通知机制,通过queue
取到这些reference
,来做额外的处理工作。
但是我们可以从上面的代码中发现,如果referent
被回收,而Reference
恰好又是Cleaner
实例,就不会进入ReferenceQueue
,而是直接调用clean
方法。
所以我们可以总结一下,DirectByteBuffer
和Cleaner
的关系就是当DirectByteBuffer
被回收的时候(这里指的是没有强引用关联这个Buffer),释放我们申请的堆外内存,来保证我们不会产生对外内存泄露。
Netty的no cleaner策略
我们说过了有cleaner
的方式,已经觉得设计的非常完善了,保证我们的堆外内存不会泛滥,而且保证了在DirectByteBuffer
被回收的时候,堆外内存也可以回收(当然如果内存泄露了是两码事)。
来看看入口,入口在UnpooledByteBufAllocator
的newDirectBuffer
方法里:
1 | if (PlatformDependent.hasUnsafe()) { |
如果可以使用unsafe
并且开启noCleaner
策略,使用InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf
。关于不能使用noCleaner
的场景我们就不细说了,实际上是在释放的时候直接使用Cleaner.clean
来进行内存释放。释放的方式是通过反射获取clean
这个方法或者内部的cleaner
这个字段,来执行方法或者获取Cleaner
对象后调用clean
。可以看下CleanerJava6
这个类。
那么说说我们的主角,noCleaner
下的DirectByteBuffer
,这时候我们创建的对象是InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf
的实例,我们关注一下如何释放:
1 | protected void freeDirect(ByteBuffer buffer) { |
这个链路非常简单。总结一下no cleaner策略和has cleaner的区别:
no cleaner
尝试获取Cleaner
字段或者Cleaner
的clean
方法,执行释放。has cleaner
直接调用unsafe
来释放堆外内存。
那么可能有人要问为什么了,为什么还要使用no cleaner
策略呢(这是Netty的默认策略)?
我认为原因有二:
- Netty实现了引用计数,我们不需要jdk帮助我们进行内存回收。这也是为什么Netty自己实现了泄露检测。由于希望释放由我们自己来操作,所以没有必要使用
Cleaner
。 - 性能的一点点提升,在使用
no cleaner
策略的时候,Netty会通过反射得到DirectByteBuffer
的构造函数,不过获取到的构造函数是两个入参的构造函数,非常简单,可以看下private DirectByteBuffer(long addr, int cap)
这个方法,不会进行创建Cleaner
等措施。
另外,可能Netty认为jdk的Bits
的reserveMemory
设计的也不怎么样(里面有我们经常说到的手动调用System.gc
的问题),所以就放弃了使用原生的Cleaner
策略。