Netty的堆外内存(非池化)的回收机制
讲解一下netty使用的堆外内存ByteBuffer是怎么gc的
Netty有多种内存管理机制:池化和非池化;jvm堆内存和jvm堆外内存。
1 | public static ByteBuffer allocateDirect(int capacity) { |
这个方法就是获取堆外内存ByteBuffer
一般线上开发推荐使用jvm堆外内存。
jvm堆外内存有几个好处:
1.netty主要是基于传输才搞了一个这个内存机制,所以主要消耗还是在socket/io上的,一般socket/io分为几个步骤:用户态切换到内核态,将数据(ByteBuf存储)拷贝到内核态,然后将内核数据拷贝到socket缓冲区,然后发送。如果使用堆外内存的话,由于数据本来就在内核缓冲中,所以省去一部拷贝进内核缓冲的操作,高并发的情况下,性能提升也是可观的。
这个是DirectByteBuffer的关联图,其在jvm堆内创建的只是一个指向堆外内存实际存储的引用,基本没有内存消耗。
2.ByteBuf的回收不需要jvm托管,减少gc压力,下面会详细讲解堆外内存gc流程。
但是堆外内存也是有缺点的:
堆外内存的回收也是基于其被对内引用对象的回收(基于虚引用),如果堆内的关联对象一直不gc,那么堆外的这块内存就相当于泄露掉了。
堆外内存(非池化)GC机制:
1 | DirectByteBuffer(int cap) { // package-private |
主要关注上面的Cleaner.create方法
他在向堆外内存申请ByteBuffer方法的时候会同时创建一个Cleaner,这个对象继承PhantomReference,表示这个对象是一个虚引用。
1 | private static class Deallocator |
创建cleaner的同时,还传入一个Deallocator对象这个对象主要是用作线程的run方法,他的run方法里有个unsafe,freeMemory(address)方法,这个方法就是调用native方法释放堆外内存。
那什么时候会触发这个run()方法呢。
如果一个DirectByteBuffer对象失去引用的时候,在ygc的时候就会回收,他内部还有一个cleaner对象,这是个虚引用对象,如果cleaner当前没有任何对象引用他,他就会将这个cleaner扔入一个队列(jvm后台有个守护线程用户维护没有引用的虚引用,并调用他的clean方法)
1 | public void clean() { |
看一下cleaner的clean方法
里面的thunk.run()就是调用Deallocator的run()方法。
现在就应该一目了然了。
由此可见只要是gc就能回收已不可达的directByteBuffer,然后对应的堆外内存也会被调用释放(YGC和FGC都能释放堆外内存,网上的一些关于只有FGC才会回收堆外内存是错误的),只是如果directbytebuffer一不小心进入老年代,而系统又迟迟不触发fgc,那只能外部显示调用system.gc()来触发老年代中的堆外内存回收(手动只能通过sys.gc()触发fgc,没有ygc触发方法)。