Netty堆外内存通过DirectByteBuffer实现管理, 它会首先申请16M的直接内存块大小, 放入DirectByteBuffer, 由PoolChunk映射这16MB的内存块, 通过PoolChunk的分配来完成该直接内存块使用与释放。 每当用户申请小块内存时, 都从这16M的内存中分配, 当该部分内存使用完后, 会释放到PoolChunk内存池中, 而不是彻底释放。 可以看出, netty每次释放直接内存并没有使用DirectByteBuffer自带Cleaner来释放(具体可以参考DirectByteBuffer堆外内存详解), 使用PoolChunk管理直接内存的使用情况的好处也是很清晰的: 直接申请与释放堆外内存是个很大的开销, 若通过PoolChunk管理直接内存使用后, 可以循环使用该部分直接内存, 这样才能满足netty的高性能特性。 本文将讲述netty释放直接内存的原理及细节。
而DirectByteBuffer封装在PooledUnsafeDirectByteBuf, netty层面也主要操作后者, 两者的关系图如下:
了解这两者对应关系, 对理解该文有一定的帮助。
PlatformDependent及PlatformDependent0简介
PlatformDependent及PlatformDependent0主要是用来确定重要参数配置的, 比如netty是否需要使用Unsfa, 当前使用的java版本等, 了解这些参数变量, 有助于更方面了解直接内存的使用。
PlatformDependent
1 | //是否有Unsafe, 拥有了Unsafe, 我们可以方便的操控直接内存,可以通过-Dio.netty.noUnsafe及-Dio.netty.tryUnsafe参数来决定, 默认是可以 |
我们可以看下CleanerJava6如何进行直接内存释放的:
1 | final class CleanerJava6 implements Cleaner { |
使用CleanerJava6释放直接内存的一个前提就是存在cleaner成员变量, 当使用DirectByteBuffer(int cap)产生的DirectByteBuffer, 其cleaner才会存在。 而使用private DirectByteBuffer(long addr, int cap)则不会产生cleaner对象, 而netty默认使用后者产生DirectByteBuffer对象(见allocateNormal.allocateDirect())。
PlatformDependent0
1 | //与Unsafe配合, 可以获取任何对象任何成员变量的值 |
直接内存的释放
当数据通过channel发送出去后(见Netty Http通信源码二(编码)分析), 然后就开始准备着释放直接内存
1 | // Release the fully written buffers, and update the indexes of the partially written buffer. |
最后调用的remove():
1 | public boolean remove() { |
remove主要做了三件事:
- 调用removeEntry(e)清理ChannelOutboundBuffer里面的缓存链表。
- 调用ReferenceCountUtil.safeRelease(msg)释放该对象的引用次数, 当引用次数为0时, 那么将直接调用PooledByteBuf.deallocate()释放该ByteBuffer。
- 调用e.recycle()来回收Entry(原理见Ndsdsdsd源码二(编码)分析)
1
2
3
4
5
6
7
8
9
10
11protected final void deallocate() {
if (handle >= 0) {
final long handle = this.handle;
this.handle = -1;
memory = null;
tmpNioBuf = null;
chunk.arena.free(chunk, handle, maxLength, cache);
chunk = null;
recycle(); //释放
}
}
主要做了两件事:
- 释放直接内存DirectByteBuffer占用的内存。
- 释放PooledUnsafeDirectByteBuf对象, 使其回收进入对象池以便下次继续使用该对象。
我们接着看area.free()是怎么操作的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16void free(PoolChunk<T> chunk, long handle, int normCapacity, PoolThreadCache cache) {
if (chunk.unpooled) {
int size = chunk.chunkSize();
destroyChunk(chunk);
activeBytesHuge.add(-size);
deallocationsHuge.increment();
} else {
SizeClass sizeClass = sizeClass(normCapacity);
if (cache != null && cache.add(this, chunk, handle, normCapacity, sizeClass)) { //放入该cache的缓存队列
// cached so not free it.
return;
}
freeChunk(chunk, handle, sizeClass);
}
}
这里做判断, 针对该对象是否池化做了不同判断:
- 针对非池化内存, 直接将该内存块给释放了
- 针对池化内存:
- 检查内存属性为tiny、small、normal中的一种
- 查找是否有该属性的缓存, 在Netty PoolThreadCache原理探究我们知道, 缓存的范围只在[16B, 32kB]之间, 若内存块在这范围之内, 则将内存块放入对应的缓存中
- 若内存块>32KB, 那么将调用freeChunk()该内存块释放到公共内存池中。
1
2
3
4
5
6
7
8
9
10void freeChunk(PoolChunk<T> chunk, long handle, SizeClass sizeClass) {
final boolean destroyChunk;
synchronized (this) {
destroyChunk = !chunk.parent.free(chunk, handle);
}
if (destroyChunk) {
// destroyChunk not need to be called while holding the synchronized lock.
destroyChunk(chunk);
}
}
freeChunk做了如下检查:
调用free来释放PoolChunkList中对应的节点
1 | boolean free(PoolChunk<T> chunk, long handle) { |
释放PoolChunk对应的16M的内存块的过程如下:
1 | protected void destroyChunk(PoolChunk<ByteBuffer> chunk) { |
总结
整个netty池化内存回收过程如下:
netty默认释放管理直接内存方式与DirectByteBuffer默认释放内存的方式不一致, 释放时会依次检查缓存、公共内存池, 若Poolchunk使用率为0, 那么16M直接内存将直接释放。