Netty 非池化内存分配

前言

非池化内存的分配由UnpooledByteBufAllocator负责,本文梳理下由其负责分配的堆内存和堆外内存如何实现的 。
Netty在非池化堆内存分配上Java9与Java8以下版本有啥不同呢?Netty堆外内存回收默认机制使用JDK提供的Cleaner吗?
大家随便看看,可以添加老梁微信「gaoliang1719」建立链接。

一、 非池化堆内内存分配

下面这小段代码摘自UnpooledByteBufAllocator#newHeapBuffer,通过此方法分析非池化堆内存的分配。

@Override
protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
return PlatformDependent.hasUnsafe() ?
new InstrumentedUnpooledUnsafeHeapByteBuf(this, initialCapacity, maxCapacity) :
new InstrumentedUnpooledHeapByteBuf(this, initialCapacity, maxCapacity);
}

解读:堆内内存分配由newHeapBuffer方法负责,如果平台支持Unsafe则创建InstrumentedUnpooledUnsafeHeapByteBuf,否则创建
InstrumentedUnpooledHeapByteBuf,下图为非池化相关类图,分别从两个类UnpooledDirectByteBuf和UnpooledHeapByteBuf延伸开来。

还是聚集到堆内存的分配上来,主要分析上图中红色部分。InstrumentedUnpooledUnsafeHeapByteBuf和InstrumentedUnpooledHeapByteBuf有啥区别?

InstrumentedUnpooledUnsafeHeapByteBuf

下面看下InstrumentedUnpooledUnsafeHeapByteBuf其内存分配的行为allocateArray().

注解@1调用了父类UnpooledUnsafeHeapByteBuf的allocateArray()

注解@2父类UnpooledUnsafeHeapByteBuf调用了PlatformDependent#allocateUninitializedArray

注解@3/@4Java9以上版本:如果待分配的内存小于1K使用堆内存,待分配的内存大于等于1K使用堆外内存。
Java8以及以下版本全部在堆内存分配

小结:使用InstrumentedUnpooledUnsafeHeapByteBuf进行内存分配时:
Java9以及以上版本:如果待分配的内存小于1K使用堆内存;待分配的内存大于等于1K使用堆外内存(调用底层PlatformDependent#allocateUninitializedArray)。
Java8以及以下版本:使用堆内存分配。

InstrumentedUnpooledHeapByteBuf

下面为InstrumentedUnpooledHeapByteBuf的内存分配allocateArray().

注解@1调用父类 UnpooledHeapByteBuf的内存分配

注解@2UnpooledHeapByteBuf的通过new byte直接在堆内存分配

小结:InstrumentedUnpooledHeapByteBuf直接在堆内存分配空间。

数据获取方式

UnpooledUnsafeHeapByteBuf数据获取

UnpooledUnsafeHeapByteBuf的数据获取方式getByte()

 @Override
protected byte _getByte(int index) {
return UnsafeByteBufUtil.getByte(array, index);
}

该方法调用UnsafeByteBufUtil的getByte,跟进去看下

static byte getByte(byte[] array, int index) {
return PlatformDependent.getByte(array, index);
}

底层通过UNSAFE.getByte这种地址+偏移量的方式获取内存中的数据。

static byte getByte(byte[] data, int index) {
return UNSAFE.getByte(data, BYTE_ARRAY_BASE_OFFSET + index);
}

UnpooledHeapByteBuf数据获取

UnpooledHeapByteBuf数据获取方式_getByte()

@Override
protected byte _getByte(int index) {
return HeapByteBufUtil.getByte(array, index);
}

该方法调用HeapByteBufUtil.getByte,跟进去看下,即直接从数组中获取数据。

static byte getByte(byte[] memory, int index) {
return memory[index];
}

小结:UnpooledUnsafeHeapByteBuf通过UNSAFE.getByte这种地址+偏移量的方式获取内存中的数据;UnpooledHeapByteBuf通过数组直接从堆内存获取。

非池化堆内存分配总结

当使用Netty非池化进行堆内存分配时:
1.Java8及其以下版本:直接在堆空间分配内存。
2.Java9及其以上版本:如果系统支持Unsafe时(通常都是支持的),对于小于1K(默认)的在堆内存分配,大于1K的分配堆外内存;如果系统不支持Unsafe直接在堆内存分配;默认大小阈值可以通过-Dio.netty.uninitializedArrayAllocationThreshold来指定,默认为1024字节。
3.堆内存数据获取通过数组实现;堆外内存获取通过UNSAFE.getByte这种地址+偏移量的方式获取。
二、非池化堆外内存分配
下面这段代码摘自UnpooledByteBufAllocator#newDirectBuffer方法,通过此方法分析非池化堆外存的分配。

@Override
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
final ByteBuf buf;
if (PlatformDependent.hasUnsafe()) {
buf = noCleaner ? new InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf(this, initialCapacity, maxCapacity) :
new InstrumentedUnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity);
} else {
buf = new InstrumentedUnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
}
return disableLeakDetector ? buf : toLeakAwareBuffer(buf);
}

解读:平台不支持支持Unsafe,构造InstrumentedUnpooledDirectByteBuf;平台支持Unsafe并且noCleaner=true构造InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf;平台支持Unsafe并且noCleaner=false,构造InstrumentedUnpooledUnsafeDirectByteBuf。那问题来了,这三个有啥区别呢?

noCleaner

 noCleaner = tryNoCleaner && PlatformDependent.hasUnsafe()
&& PlatformDependent.hasDirectBufferNoCleanerConstructor();

解读:三个判断条件一个一个来看:
@1 tryNoCleaner=PlatformDependent.useDirectBufferNoCleaner()该方法在前一篇文章中也分析过,当maxDirectMemory!=0 && 支持Unsafe && DirectByteBuffer的构造函数可用时,tryNoCleaner = true。
@2 PlatformDependent.hasUnsafe() 在上一篇文章中分析过具体 UNSAFE_UNAVAILABILITY_CAUSE == null,平台不支持UNSAFE会将异常封装在UNSAFE_UNAVAILABILITY_CAUSE中,等于null意味着平台支持Unsafe。
@3 PlatformDependent.hasDirectBufferNoCleanerConstructor() 指的是通过反射DirectByteBuffer构造器对象是否可用。
所以看出,通常在实践中(Linux、JDK8)上面这些条件都是支持的,也就是Netty默认noCleaner为true。

InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf

下面看下堆外内存的分配和回收:

注解@1堆外内存分配底层调用了PlatformDependent0#allocateDirectNoCleaner方法,malloc()返回获得内存空间的首地址,失败返回null,然后根据返回的内存地址调用DirectByteBuffer构造函数分配堆外内存。

static ByteBuffer allocateDirectNoCleaner(int capacity) {
// Calling malloc with capacity of 0 may return a null ptr or a memory address that can be used.
// Just use 1 to make it safe to use in all cases:
// See: http://pubs.opengroup.org/onlinepubs/009695399/functions/malloc.html

/**
* malloc()返回获得内存空间的首地址,失败返回null
*/

return newDirectBuffer(UNSAFE.allocateMemory(Math.max(1, capacity)), capacity);
}

注解@2堆外内存释放底层调用了PlatformDependent0#freeMemory方法,通过UNSAFE.freeMemory释放堆外内存。

static void freeMemory(long address) {
UNSAFE.freeMemory(address);
}

InstrumentedUnpooledUnsafeDirectByteBuf

下面看下堆外内存的分配与回收

注解@1底层调用ByteBuffer#allocateDirect来分配堆外内存,具体为直接new DirectByteBuffer()

public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}

注解@2释放堆外内存调用了PlatformDependent#freeDirectBuffer()底层调用CLEANER.freeDirectBuffer实现

public static void freeDirectBuffer(ByteBuffer buffer) {
CLEANER.freeDirectBuffer(buffer);
}

nstrumentedUnpooledDirectByteBuf

注解@1堆外内存分配同InstrumentedUnpooledUnsafeDirectByteBuf,通过父类UnpooledDirectByteBuf#allocateDirect调用ByteBuffer#allocateDirect来分配堆外内存,堆内存直接new DirectByteBuffer

public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}

注解@2堆外内存释放同InstrumentedUnpooledUnsafeDirectByteBuf,通过父类UnpooledDirectByteBuf#freeDirect调用底层调用CLEANER.freeDirectBuffer实现。

public static void freeDirectBuffer(ByteBuffer buffer) {
CLEANER.freeDirectBuffer(buffer);
}

小结:@1 InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf的内存释放调用UNSAFE.freeMemory(address)实现
@2 InstrumentedUnpooledUnsafeDirectByteBuf和nstrumentedUnpooledDirectByteBuf的内存释放是一样的,使用了JDK提供的CLEANER.freeDirectBuffer(buffer)。
@3 Netty默认自行管理堆外内存的分配与释放,并未使用JDK提供的释放方式,而是通过底层API自行释放。

堆外内存数据获取

下面是三个buffer获取内存数据的方式:
InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf通过UnpooledUnsafeDirectByteBuf#_getByte()获取数据
InstrumentedUnpooledUnsafeDirectByteBuf通过UnpooledUnsafeDirectByteBuf#_getByte()获取数据
nstrumentedUnpooledDirectByteBuf通过UnpooledDirectByteBuf#_getByte()获取数据
InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf和InstrumentedUnpooledUnsafeDirectByteBuf获取方式一样;下面看下这两类获取方式。

UnpooledUnsafeDirectByteBuf获取内存数据

protected byte _getByte(int index) {
return UnsafeByteBufUtil.getByte(addr(index)); // 注解@1
}

注解@1底层通过UNSAFE.getByte(address)这种“地址+偏移量” 的方式获取内存数据。

UnpooledDirectByteBuf获取内存数据

protected byte _getByte(int index) {
return buffer.get(index); // 注解@2
}

注解@2通过ByteBuffer#get方式获取,底层通过Bits#unsafe.getByte(address)获取内存数据。

非池化堆外内存总结

Netty在堆外内存分配上,在系统支持的情况下,默认自己通过UNSAFE.freeMemory去释放内存,也就是noCleaner,没有使用JDK提供的Cleaner释放机制。至于为啥netty选择自己实现,不用JDK提供的方式,主要考虑性能原因,与JDK Bits设计有关系。