ByteBuf 的数据模型、生命周期和内存分配
1. ByteBuf 索引与扩容
测试代码:
java
@Test
public void test1() {
ByteBuf buf = Unpooled.heapBuffer(4, 16);
buf.writeInt(100);
buf.writeByte(1); // 触发扩容
int value = buf.readInt();
buf.release();
}
创建堆内存 ByteBuf
1.1 初始化缓冲区
java
public static ByteBuf heapBuffer(int initialCapacity, int maxCapacity) {
return ALLOC.heapBuffer(initialCapacity, maxCapacity);
}
/**
* 分配一个堆内存 {@link ByteBuf},其初始容量为指定的 initialCapacity,
* 最大容量为指定的 maxCapacity。
*/
ByteBuf heapBuffer(int initialCapacity, int maxCapacity);
AbstractByteBufAllocator.heapBuffer 负责参数校验并调用抽象的 newHeapBuffer:
java
/**
* 分配一个堆内存 {@link ByteBuf},其初始容量为指定的 initialCapacity,
* 最大容量为指定的 maxCapacity。
*/
public ByteBuf heapBuffer(int initialCapacity, int maxCapacity) {
if (initialCapacity == 0 && maxCapacity == 0) {
return emptyBuf;
}
validate(initialCapacity, maxCapacity);
return newHeapBuffer(initialCapacity, maxCapacity);
}
UnpooledByteBufAllocator.newHeapBuffer 提供具体的非池化堆内存实现:
java
@Override
protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
return PlatformDependent.hasUnsafe() ?
new InstrumentedUnpooledUnsafeHeapByteBuf(this, initialCapacity, maxCapacity) :
new InstrumentedUnpooledHeapByteBuf(this, initialCapacity, maxCapacity);
}
如果当前 JVM 可以使用 Netty 的 Unsafe 路径,创建的便是 InstrumentedUnpooledUnsafeHeapByteBuf。下面沿着该实现进行分析。
该类的继承关系为:ByteBuf -> AbstractByteBuf -> AbstractReferenceCountedByteBuf -> UnpooledHeapByteBuf -> UnpooledUnsafeHeapByteBuf -> InstrumentedUnpooledUnsafeHeapByteBuf。
各个类的职责如下:
ByteBuf(核心抽象接口) -> 定义 Netty 中字节容器的标准 API 契约。
AbstractByteBuf(基础逻辑实现) -> 维护读索引、写索引、标记索引等状态,并实现通用读写流程和边界检查。
AbstractReferenceCountedByteBuf(生命周期管理) -> 负责引用计数;引用计数归零时调用具体子类的 deallocate()。
UnpooledHeapByteBuf(非池化堆内存实现) -> 以独占的 byte[] 作为底层存储,扩容时创建新数组并复制数据。
UnpooledUnsafeHeapByteBuf(Unsafe 访问优化) -> 在平台支持时优化部分字节访问操作。
InstrumentedUnpooledUnsafeHeapByteBuf(内存统计) -> 在数组分配和释放时更新 allocator 的堆内存统计;Instrumented 不表示它只能用于测试。
1.2 写入方法与扩容
以写入 int 为例,调用 AbstractByteBuf.writeInt:
java
@Override
public ByteBuf writeInt(int value) {
// 确保写入容量足够
ensureWritable0(4);
// 在当前 writerIndex 的绝对位置写入数据
_setInt(writerIndex, value);
// 写索引前移 4 个字节
writerIndex += 4;
return this;
}
下面看一下确保可写容量的方法:
java
final void ensureWritable0(int minWritableBytes) {
final int writerIndex = writerIndex();
// 当前 writerIndex 加上需要写入的字节数,得到目标容量
final int targetCapacity = writerIndex + minWritableBytes;
// 使用非短路 & 来减少分支 ------ 这是热点路径,targetCapacity 很少会溢出
if (targetCapacity >= 0 & targetCapacity <= capacity()) {
ensureAccessible();
return;
}
// 检查边界值
if (checkBounds && (targetCapacity < 0 || targetCapacity > maxCapacity)) {
ensureAccessible();
throw new IndexOutOfBoundsException(String.format(
"writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
writerIndex, minWritableBytes, maxCapacity, this));
}
// 优先使用快速扩容额度;不足时交由 allocator 计算新容量。
final int fastWritable = maxFastWritableBytes();
int newCapacity = fastWritable >= minWritableBytes ? writerIndex + fastWritable
: alloc().calculateNewCapacity(targetCapacity, maxCapacity);
// 调整容量
capacity(newCapacity);
}
当容量不足时,会执行扩容逻辑:
java
public int calculateNewCapacity(int minNewCapacity, int maxCapacity) {
checkPositiveOrZero(minNewCapacity, "minNewCapacity");
// 如果最小新容量大于允许的最大容量,则抛出异常
if (minNewCapacity > maxCapacity) {
throw new IllegalArgumentException(String.format(
"minNewCapacity: %d (expected: not greater than maxCapacity(%d)",
minNewCapacity, maxCapacity));
}
/*
* 作为 ByteBuf 动态扩容时的"尺度阈值"(大小固定为 4 MiB)
* 当 minNewCapacity 小于 4 MiB 时,Netty 采用指数级倍增的扩容策略
* 当 minNewCapacity 大于 4 MiB 时,Netty 采用固定 4 MiB 的扩容策略
*/
final int threshold = CALCULATE_THRESHOLD; // 4 MiB page
if (minNewCapacity == threshold) {
return threshold;
}
if (minNewCapacity > threshold) {
int newCapacity = minNewCapacity / threshold * threshold;
if (newCapacity > maxCapacity - threshold) {
newCapacity = maxCapacity;
} else {
newCapacity += threshold;
}
return newCapacity;
}
// 以 max(minNewCapacity, 64) 为下限,计算最小的 2 的正整数次幂。
final int newCapacity = MathUtil.findNextPositivePowerOfTwo(Math.max(minNewCapacity, 64));
return Math.min(newCapacity, maxCapacity);
}
UnpooledUnsafeHeapByteBuf._setInt 最终会调用 UnsafeByteBufUtil.setInt:
java
static void setInt(byte[] array, int index, int value) {
// 支持非对齐访问时,使用平台相关的批量 int 访问。
if (UNALIGNED) {
PlatformDependent.putInt(array, index, BIG_ENDIAN_NATIVE_ORDER ? value : Integer.reverseBytes(value));
// 某些 JDK 上可使用 VarHandle 访问。
} else if (USE_VAR_HANDLE) {
VarHandleByteBufferAccess.setIntBE(array, index, value);
} else {
// 回退到逐字节写入,仍保持大端字节序语义。
PlatformDependent.putByte(array, index, (byte) (value >>> 24));
PlatformDependent.putByte(array, index + 1, (byte) (value >>> 16));
PlatformDependent.putByte(array, index + 2, (byte) (value >>> 8));
PlatformDependent.putByte(array, index + 3, (byte) value);
}
}
在 Unsafe 路径中,PlatformDependent.putInt 会继续委派给平台相关实现,最终可能使用 Unsafe.putInt。也有可能根据 Netty 版本、JDK版本发生变化
writeByte 与 writeInt 的流程相同,只是写入长度为 1 个字节。
与 set 方法相比,write 方法会先确保可写容量,并推动 writerIndex 前移。
1.3 读取方法
AbstractByteBuf.readInt:
java
public int readInt() {
// 检查是否有足够可读字节
checkReadableBytes0(4);
// 从当前读索引读取数据
int v = _getInt(readerIndex);
// int 占 4 个字节,读索引前移 4
readerIndex += 4;
return v;
}
下面简单看一下 checkReadableBytes0 方法:
java
private void checkReadableBytes0(int minimumReadableBytes) {
// 检查 ByteBuf 是否仍可访问,例如没有被 release。
ensureAccessible();
// 检查 readerIndex + 本次读取长度是否超过 writerIndex。
if (checkBounds && readerIndex > writerIndex - minimumReadableBytes) {
throw new IndexOutOfBoundsException(String.format(
"readerIndex(%d) + length(%d) exceeds writerIndex(%d): %s",
readerIndex, minimumReadableBytes, writerIndex, this));
}
}
读取 int 的调用链为:
UnpooledUnsafeHeapByteBuf._getInt -> UnsafeByteBufUtil.getInt。
该方法同样会根据平台能力选择不同实现,语义与写入时一致:按 ByteBuf 的字节序读取连续 4 个字节。
java
static int getInt(byte[] array, int index) {
if (UNALIGNED) {
int v = PlatformDependent.getInt(array, index);
return BIG_ENDIAN_NATIVE_ORDER ? v : Integer.reverseBytes(v);
}
if (USE_VAR_HANDLE) {
return VarHandleByteBufferAccess.getIntBE(array, index);
}
return PlatformDependent.getByte(array, index) << 24 |
(PlatformDependent.getByte(array, index + 1) & 0xff) << 16 |
(PlatformDependent.getByte(array, index + 2) & 0xff) << 8 |
PlatformDependent.getByte(array, index + 3) & 0xff;
}
在 Unsafe 路径中,PlatformDependent.getInt 会继续委派给平台相关实现,最终可能使用 Unsafe.getInt。
总结:与 get 方法相比,read 方法会推动 readerIndex 前移,同时检查读取范围是否合法。
1.4 引用计数释放与内存统计
释放流程如下:
AbstractReferenceCountedByteBuf.release:
java
public boolean release() {
return handleRelease(RefCnt.release(refCnt));
}
RefCnt.release(refCnt) 会以原子方式减少引用计数。引用计数实现可根据运行环境选择 Unsafe、VarHandle 或原子字段更新器:代码如下
java
public static int refCnt(RefCnt ref) {
switch (REF_CNT_IMPL) {
case UNSAFE:
return UnsafeRefCnt.refCnt(ref);
case VAR_HANDLE:
return VarHandleRefCnt.refCnt(ref);
case ATOMIC_UPDATER:
default:
return AtomicRefCnt.refCnt(ref);
}
}
接着,Unsafe 实现会进入类似 UnsafeRefCnt.release -> RefCnt.release -> RefCnt.release0 的流程:
java
private static boolean release0(RefCnt instance, int decrement) {
int curr, next;
do {
// 读取当前的内部引用计数编码值。
curr = PlatformDependent.getInt(instance, VALUE_OFFSET);
// curr == decrement 表示本次释放的是最后一个对外可见引用。
// Netty 不会将内部值写为 0,而是写为 1:奇数表示对象已释放,
if (curr == decrement) {
next = 1;
} else {
// curr < decrement 表示当前引用数不足,(curr & 1) == 1 表示对象已经释放。
if (curr < decrement || (curr & 1) == 1) {
throwIllegalRefCountOnRelease(decrement, curr);
}
// 内部编码以 2 表示一个对外可见引用,因此普通 release 按 decrement(通常为 2)递减。
next = curr - decrement;
}
// CAS 保证多线程下的递减操作是原子的;若其他线程已修改该值,则重新读取并计算。
} while (!PlatformDependent.compareAndSwapInt(instance, VALUE_OFFSET, curr, next));
// 写入奇数表示本次 release 使引用计数归零,调用方随后会执行 deallocate()。
return (next & 1) == 1;
}
该方法通过 CAS 原子递减引用计数;返回 true 时表示本次 release 已使对象进入释放状态,handleRelease 随后会调用 deallocate()。
AbstractReferenceCountedByteBuf.handleRelease 在引用计数归零后,最终调用 UnpooledHeapByteBuf.deallocate:
java
protected void deallocate() {
freeArray(array);
array = EmptyArrays.EMPTY_BYTES;
}
UnpooledHeapByteBuf.freeArray 会处理底层数组的释放逻辑。对于 InstrumentedUnpooledUnsafeHeapByteBuf,还会减少 allocator 的堆内存统计:
java
protected void freeArray(byte[] array) {
int length = array.length;
super.freeArray(array);
((UnpooledByteBufAllocator) alloc()).decrementHeap(length);
}
UnpooledByteBufAllocator.decrementHeap:
java
void decrementHeap(int amount) {
metric.heapCounter.add(-amount);
}
这里减少的是 allocator 记录的 ByteBuf 堆数组容量。非池化堆缓冲区在 release() 后会解除对数组的持有,但 byte[] 何时真正由 JVM 回收仍取决于 GC;因此 release() 不等同于立即回收 Java 堆内存。
2. 派生视图、复制与引用计数
2.1 示例:slice、retainedSlice 与 copy
测试代码:
java
ByteBuf parent = Unpooled.copiedBuffer("hello", StandardCharsets.UTF_8);
ByteBuf slice = parent.slice(0, 2);
ByteBuf retainedSlice = parent.retainedSlice(0, 2);
ByteBuf copy = parent.copy(0, 2);
System.out.println(parent.refCnt()); // retainedSlice 后为 2
parent.release(); // retainedSlice 仍持有共享内存,slice 与 retainedSlice 均可读取
retainedSlice.release(); // 引用计数归零
copy.release(); // copy 独立释放
2.2 copiedBuffer 的 UTF-8 编码过程
首先,调用链为:Unpooled.copiedBuffer -> Unpooled.copiedBufferUtf8 -> ByteBufUtil.reserveAndWriteUtf8 -> ByteBufUtil.reserveAndWriteUtf8Seq -> ByteBufUtil.writeUtf8 -> ByteBufUtil.unsafeWriteUtf8。
该过程进行了多项优化:它会根据字符串中不同的字符类型选择不同的编码路径;其中还会通过 ByteBufUtil.utf8Bytes(string) 计算字符串按 UTF-8 编码后的长度。
下面简单看一下这部分代码:
java
private static int unsafeWriteUtf8(byte[] buffer, long memoryOffset, int writerIndex,
CharSequence seq, int start, int end) {
assert !(seq instanceof AsciiString);
long writerOffset = memoryOffset + writerIndex;
final long oldWriterOffset = writerOffset;
for (int i = start; i < end; i++) {
char c = seq.charAt(i);
// 判断字符是否属于 ASCII 字符集。
if (c < 0x80) {
PlatformDependent.putByte(buffer, writerOffset++, (byte) c);
// 判断字符是否属于 扩展的拉丁语系、希腊语、西里尔字母等。
} else if (c < 0x800) {
PlatformDependent.putByte(buffer, writerOffset++, (byte) (0xc0 | (c >> 6)));
PlatformDependent.putByte(buffer, writerOffset++, (byte) (0x80 | (c & 0x3f)));
// 判断当前字符是否属于 代理对(Surrogate Pair)的高位代理(High Surrogate)
} else if (isSurrogate(c)) {
if (!Character.isHighSurrogate(c)) {
PlatformDependent.putByte(buffer, writerOffset++, WRITE_UTF_UNKNOWN);
continue;
}
// Surrogate Pair consumes 2 characters.
if (++i == end) {
PlatformDependent.putByte(buffer, writerOffset++, WRITE_UTF_UNKNOWN);
break;
}
char c2 = seq.charAt(i);
// Extra method is copied here to NOT allow inlining of writeUtf8
// and increase the chance to inline CharSequence::charAt instead
if (!Character.isLowSurrogate(c2)) {
PlatformDependent.putByte(buffer, writerOffset++, WRITE_UTF_UNKNOWN);
PlatformDependent.putByte(buffer, writerOffset++,
(byte) (Character.isHighSurrogate(c2) ? WRITE_UTF_UNKNOWN : c2));
} else {
int codePoint = Character.toCodePoint(c, c2);
// See https://www.unicode.org/versions/Unicode7.0.0/ch03.pdf#G2630.
PlatformDependent.putByte(buffer, writerOffset++, (byte) (0xf0 | (codePoint >> 18)));
PlatformDependent.putByte(buffer, writerOffset++, (byte) (0x80 | ((codePoint >> 12) & 0x3f)));
PlatformDependent.putByte(buffer, writerOffset++, (byte) (0x80 | ((codePoint >> 6) & 0x3f)));
PlatformDependent.putByte(buffer, writerOffset++, (byte) (0x80 | (codePoint & 0x3f)));
}
// 处理常规的 3 字节字符(如绝大多数的中文汉字、日文假名、韩文等)。
} else {
PlatformDependent.putByte(buffer, writerOffset++, (byte) (0xe0 | (c >> 12)));
PlatformDependent.putByte(buffer, writerOffset++, (byte) (0x80 | ((c >> 6) & 0x3f)));
PlatformDependent.putByte(buffer, writerOffset++, (byte) (0x80 | (c & 0x3f)));
}
}
return (int) (writerOffset - oldWriterOffset);
}
2.3 slice视图
下面看一下 slice 方法:
调用链为:ByteBuf.slice -> UnpooledSlicedByteBuf -> AbstractUnpooledSlicedByteBuf。
slice() 仅创建共享底层存储的视图,不会增加引用计数。当共享引用计数因最后一次 release() 降为 0 时,普通 slice 也随之不可访问。本示例中,retainedSlice() 已额外保留一次引用,因此 parent.release() 后共享存储仍然可访问。
java
AbstractUnpooledSlicedByteBuf(ByteBuf buffer, int index, int length) {
super(length);
// 检查边界是否越界
checkSliceOutOfBounds(index, length, buffer);
// 若 buffer 已经是 slice 视图,则直接复用其根 buffer 并累加偏移量。
if (buffer instanceof AbstractUnpooledSlicedByteBuf) {
this.buffer = ((AbstractUnpooledSlicedByteBuf) buffer).buffer;
adjustment = ((AbstractUnpooledSlicedByteBuf) buffer).adjustment + index;
// DuplicatedByteBuf 需要先解包到底层 buffer;当前 index 即当前切片的偏移量。
} else if (buffer instanceof DuplicatedByteBuf) {
this.buffer = buffer.unwrap();
adjustment = index;
} else {
// 普通 ByteBuf 直接作为底层 buffer。
this.buffer = buffer;
// adjustment 记录了视图 ByteBuf 相对于原生 ByteBuf 的内存起始偏移量
adjustment = index;
}
// 初始化长度
initLength(length);
// 初始化 writerIndex;readerIndex 保持为 0。
writerIndex(length);
}
2.4 retainedSlice与引用计数
retainedSlice 方法可理解为:先创建 slice 视图,再为共享的底层存储增加一次引用计数:
java
public ByteBuf retainedSlice(int index, int length) {
return slice(index, length).retain();
}
retain 方法的调用链为:AbstractByteBuf.retain -> AbstractByteBuf.retain0 -> AbstractReferenceCountedByteBuf.retain -> RefCnt.retain。
下面看一下该方法:
java
static void retain(RefCnt instance) {
retain0(instance, 2);
}
可以发现,内部计数值增加的是 2,而不是直观上理解的 1。这是为什么?
需要结合 release 方法理解:释放时同样按 2 递减,即 release0(instance, 2)。这是 Netty 的内部编码:偶数表示对象仍存活,奇数表示对象已经释放。实际计数值中确实会出现 1:最后一次释放不会将内部值设为 0,而是设为 1,以标记对象已释放。
java
private static boolean release0(RefCnt instance, int decrement) {
int curr, next;
do {
...
if (curr == decrement) {
next = 1;
}
...
} while (!PlatformDependent.compareAndSwapInt(instance, VALUE_OFFSET, curr, next));
return (next & 1) == 1;
}
2.5 copy的独立内存
接下来,看一下 copy 方法:
java
public ByteBuf copy(int index, int length) {
checkIndex(index, length);
return alloc().heapBuffer(length, maxCapacity()).writeBytes(array, index, length);
}
该方法会分配新的 ByteBuf 并复制指定范围的数据,因此可以独立管理其生命周期。
3. 堆内、堆外与非池化分配器
3.1 示例:堆内与堆外 ByteBuf
测试代码:
java
ByteBuf heap = UnpooledByteBufAllocator.DEFAULT.heapBuffer();
ByteBuf direct = UnpooledByteBufAllocator.DEFAULT.directBuffer();
System.out.println(heap.hasArray()); // true
System.out.println(direct.hasArray()); // false
heap.release();
direct.release();
UnpooledByteBufAllocator.DEFAULT.heapBuffer() 的分配过程已在前文说明,这里不再展开。
下面重点分析 UnpooledByteBufAllocator.DEFAULT.directBuffer(),即非池化直接内存 ByteBuf 的生命周期。调用链为:AbstractByteBufAllocator.directBuffer -> UnpooledByteBufAllocator.newDirectBuffer -> InstrumentedUnpooledUnsafeDirectByteBuf。
3.2 直接内存 ByteBuf 的实现层次
InstrumentedUnpooledUnsafeDirectByteBuf 的继承关系为:ByteBuf -> AbstractByteBuf -> AbstractReferenceCountedByteBuf -> UnpooledDirectByteBuf -> UnpooledUnsafeDirectByteBuf -> InstrumentedUnpooledUnsafeDirectByteBuf。
下面重点看一下 UnpooledDirectByteBuf 的构造器:
java
public UnpooledDirectByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) {
super(maxCapacity);
ObjectUtil.checkNotNull(alloc, "alloc");
checkPositiveOrZero(initialCapacity, "initialCapacity");
checkPositiveOrZero(maxCapacity, "maxCapacity");
if (initialCapacity > maxCapacity) {
throw new IllegalArgumentException(String.format(
"initialCapacity(%d) > maxCapacity(%d)", initialCapacity, maxCapacity));
}
// 设置分配器
this.alloc = alloc;
// 分配并设置底层 ByteBuffer
setByteBuffer(allocateDirectBuffer(initialCapacity), false);
}
3.3 直接内存的分配与统计
下面看一下 allocateDirectBuffer 方法:
调用链为:UnpooledDirectByteBuf.allocateDirectBuffer -> InstrumentedUnpooledUnsafeDirectByteBuf.allocateDirectBuffer。
具体实现如下:
java
protected CleanableDirectBuffer allocateDirectBuffer(int capacity) {
// 对底层直接缓冲区进行包装,以便在显式清理时同步更新 allocator 指标。
// 当 ByteBuf 最后一次 release() 时,会主动调用该包装器的 clean()。
// 若应用未显式释放,底层 DirectByteBuffer 的 Cleaner 才可能在其不可达后由 JVM 触发。
CleanableDirectBuffer buffer = super.allocateDirectBuffer(capacity);
return new DecrementingCleanableDirectBuffer(alloc(), buffer);
}
继续向下,调用链为:UnpooledDirectByteBuf.allocateDirectBuffer -> PlatformDependent.allocateDirect -> CleanerJava9.allocate。
java
public CleanableDirectBuffer allocate(int capacity) {
// 使用 CleanableDirectBufferImpl 包装 ByteBuffer.allocateDirect(capacity) 创建的直接缓冲区。
return new CleanableDirectBufferImpl(ByteBuffer.allocateDirect(capacity));
}
CleanableDirectBufferImpl 的 clean 方法会调用 freeDirectBufferStatic:
java
private static void freeDirectBufferStatic(ByteBuffer buffer) {
// Try to minimize overhead when there is no SecurityManager present.
// 没有安全管理的场景,不要有任何多余的安全检查开销。
// See https://bugs.openjdk.java.net/browse/JDK-8191053.
// 它根据 JVM 是否安装了 SecurityManager(安全管理器)走了两条完全不同的路径。
if (System.getSecurityManager() == null) {
try {
// 主动请求 JVM 清理该 DirectByteBuffer 对应的直接内存。
INVOKE_CLEANER.invokeExact(buffer);
} catch (Throwable cause) {
PlatformDependent0.throwException(cause);
}
} else {
freeDirectBufferPrivileged(buffer);
}
}
接下来,看一下 DecrementingCleanableDirectBuffer 的初始化:
java
private DecrementingCleanableDirectBuffer(
ByteBufAllocator alloc, CleanableDirectBuffer delegate) {
this(alloc, delegate, delegate.buffer().capacity());
}
private DecrementingCleanableDirectBuffer(
ByteBufAllocator alloc, CleanableDirectBuffer delegate, int capacityConsumed) {
this.alloc = (UnpooledByteBufAllocator) alloc;
this.alloc.incrementDirect(capacityConsumed);
this.delegate = delegate;
}
下面重点看一下它的 clean 方法:
java
public void clean() {
// 1. 先获取这块内存的容量
int capacity = delegate.buffer().capacity();
// 2. 调用底层清理方法,释放该 DirectByteBuffer 持有的直接内存。
delegate.clean();
// 3. 内存释放后,更新分配器的统计指标
alloc.decrementDirect(capacity);
}
除了调用委托对象的 clean(),该方法还会调用 alloc.decrementDirect(capacity),与初始化时的 this.alloc.incrementDirect(capacityConsumed) 对应,用于维护 allocator 的直接内存统计。
3.4 直接内存的释放
直接看关键代码 UnpooledDirectByteBuf.deallocate:
java
protected void deallocate() {
ByteBuffer buffer = this.buffer;
if (buffer == null) {
return;
}
this.buffer = null;
if (!doNotFree) {
if (cleanable != null) {
cleanable.clean();
cleanable = null;
} else {
freeDirect(buffer);
}
}
}
当最后一次 release() 触发 deallocate(),且 doNotFree 为 false 时,会调用 cleanable.clean(),即 DecrementingCleanableDirectBuffer.clean(),从而完成直接内存清理和 allocator 指标更新。
4. PooledByteBufAllocator 内存池
4.1 对象与内存复用示例
测试代码:
java
// Recycler 在满足条件的线程中会使用线程本地对象池;这里放到 FastThreadLocalThread 中执行,便于观察复用效果。
FastThreadLocalThread.runWithFastThreadLocal(() -> {
ByteBufAllocator allocator = PooledByteBufAllocator.DEFAULT;
System.out.println("FastThreadLocal enabled: " +
FastThreadLocalThread.currentThreadWillCleanupFastThreadLocals());
System.out.println("io.netty.recycler.maxCapacityPerThread: " +
System.getProperty("io.netty.recycler.maxCapacityPerThread"));
System.out.println("io.netty.recycler.maxCapacity: " + System.getProperty("io.netty.recycler.maxCapacity"));
// 注意:ByteBuf 的 hashCode() 按内容计算(AbstractByteBuf#hashCode -> ByteBufUtil.hashCode),
// 不能用来判断是否为同一个对象。这里使用 System.identityHashCode() 查看对象身份,避免受到重写 hashCode() 的影响。
ByteBuf first = allocator.directBuffer(128);
ByteBuf pooledFirst = first.unwrap();
System.out.println("ByteBuf implementation: " + first.getClass().getName());
System.out.println("================first:" + System.identityHashCode(first));
System.out.println("================pooled first:" + System.identityHashCode(pooledFirst));
first.writeInt(100);
assertTrue(first.release());
ByteBuf second = allocator.directBuffer(128);
try {
ByteBuf pooledSecond = second.unwrap();
System.out.println("================second:" + System.identityHashCode(second));
System.out.println("================pooled second:" + System.identityHashCode(pooledSecond));
System.out.println("leak-aware wrapper reused? " + (first == second));
System.out.println("pooled ByteBuf reused? " + (pooledFirst == pooledSecond));
assertNotSame(first, second);
assertSame(pooledFirst, pooledSecond);
assertEquals(0, second.readerIndex());
assertEquals(0, second.writerIndex());
} finally {
second.release();
}
});
4.2 分配器初始化
下面从 PooledByteBufAllocator 的初始化过程开始分析:
java
/**
* 初始化
* @param preferDirect 是否优先分配直接内存;该参数不决定内存池是否创建
* @param nHeapArena 堆内存 Arena(HeapArena)的数量。默认取 2 * CPU 核心数,并受最大堆内存限制
* (不超过 maxMemory / chunkSize / 2 / 3);为 0 表示禁用堆内存池
* @param nDirectArena 直接内存 Arena(DirectArena)的数量。默认取 2 * CPU 核心数,并受最大直接内存限制
* (不超过 maxDirectMemory / chunkSize / 2 / 3);为 0 表示禁用直接内存池
* @param pageSize 页大小,内存管理的基本单位,默认 8192(8KB),最小值 4096。
* 小于它的分配通常由 Subpage 管理;更大的分配按 Page 或连续 Page 处理
* @param maxOrder 用于计算 Chunk 大小的最大阶数,默认 9。它决定 Chunk 大小:
* chunkSize = pageSize << maxOrder(默认 8KB << 9 = 4MB)
* @param smallCacheSize 线程本地缓存中 Small 级别(小于 pageSize)每个尺寸规格缓存队列的长度,默认 256;
* 为 0 表示不使用 Small 缓存
* @param normalCacheSize 线程本地缓存中 Normal 级别(pageSize ~ chunkSize,且不超过 maxCachedBufferCapacity)
* 每个尺寸规格缓存队列的长度,默认 64;为 0 表示不使用 Normal 缓存
* @param useCacheForAllThreads 是否为所有线程(而非仅 FastThreadLocalThread)启用线程本地缓存,默认 false
* @param directMemoryCacheAlignment 直接内存的缓存对齐字节数,默认 0(表示不对齐)。必须是 2 的幂,且需要平台
* 支持对齐分配;若非 0,pageSize 会被向上调整为其整数倍
*/
public PooledByteBufAllocator(boolean preferDirect, int nHeapArena, int nDirectArena, int pageSize, int maxOrder,
int smallCacheSize, int normalCacheSize,
boolean useCacheForAllThreads, int directMemoryCacheAlignment) {
super(preferDirect);
threadCache = new PoolThreadLocalCache(useCacheForAllThreads);
this.smallCacheSize = smallCacheSize;
this.normalCacheSize = normalCacheSize;
// 直接内存对齐值为 0 时,表示不要求额外的对齐。
if (directMemoryCacheAlignment != 0) {
if (!PlatformDependent.hasAlignDirectByteBuffer()) {
throw new UnsupportedOperationException("Buffer alignment is not supported. " +
"Either Unsafe or ByteBuffer.alignSlice() must be available.");
}
// Ensure page size is a whole multiple of the alignment, or bump it to the next whole multiple.
pageSize = (int) PlatformDependent.align(pageSize, directMemoryCacheAlignment);
}
// 校验参数,并计算一个 Chunk 能容纳的总内存大小。
chunkSize = validateAndCalculateChunkSize(pageSize, maxOrder);
checkPositiveOrZero(nHeapArena, "nHeapArena");
checkPositiveOrZero(nDirectArena, "nDirectArena");
checkPositiveOrZero(directMemoryCacheAlignment, "directMemoryCacheAlignment");
if (directMemoryCacheAlignment > 0 && !isDirectMemoryCacheAlignmentSupported()) {
throw new IllegalArgumentException("directMemoryCacheAlignment is not supported");
}
if ((directMemoryCacheAlignment & -directMemoryCacheAlignment) != directMemoryCacheAlignment) {
throw new IllegalArgumentException("directMemoryCacheAlignment: "
+ directMemoryCacheAlignment + " (expected: power of two)");
}
// 校验 Page 大小,并计算对应的移位值,后续可通过位运算完成大小换算和索引计算。
int pageShifts = validateAndCalculatePageShifts(pageSize, directMemoryCacheAlignment);
// 初始化堆内 Arena:每个 Arena 管理一组堆内 PoolChunk。
if (nHeapArena > 0) {
heapArenas = newArenaArray(nHeapArena);
List<PoolArenaMetric> metrics = new ArrayList<PoolArenaMetric>(heapArenas.length);
final SizeClasses sizeClasses = new SizeClasses(pageSize, pageShifts, chunkSize, 0);
for (int i = 0; i < heapArenas.length; i++) {
PoolArena.HeapArena arena = new PoolArena.HeapArena(this, sizeClasses);
heapArenas[i] = arena;
metrics.add(arena);
}
// 创建指标列表的只读视图,避免外部修改 Arena 列表结构。
heapArenaMetrics = Collections.unmodifiableList(metrics);
} else {
heapArenas = null;
heapArenaMetrics = Collections.emptyList();
}
// 初始化直接内存 Arena:每个 Arena 管理一组直接内存 PoolChunk。
if (nDirectArena > 0) {
directArenas = newArenaArray(nDirectArena);
List<PoolArenaMetric> metrics = new ArrayList<PoolArenaMetric>(directArenas.length);
final SizeClasses sizeClasses = new SizeClasses(pageSize, pageShifts, chunkSize,
directMemoryCacheAlignment);
for (int i = 0; i < directArenas.length; i++) {
PoolArena.DirectArena arena = new PoolArena.DirectArena(this, sizeClasses);
directArenas[i] = arena;
metrics.add(arena);
}
directArenaMetrics = Collections.unmodifiableList(metrics);
} else {
directArenas = null;
directArenaMetrics = Collections.emptyList();
}
metric = new PooledByteBufAllocatorMetric(this);
}
4.3 内存分配模型
可以把申请 ByteBuf 内存的过程类比为仓储流转。这个类比描述的是"申请内存"的路径,而不是向 ByteBuf 中读写数据的过程:
- 第一站:员工桌上的私人储物柜(
PoolThreadCache) 当前线程优先查看自己的缓存。命中时可以直接取用,通常不需要访问共享的 Arena 状态。 - 第二站:分区大仓库(
PoolArena) 线程本地缓存未命中时,由对应的堆内或直接内存 Arena 负责分配。 - 第三站:小件分拣区与零件盒(
smallSubpagePools/PoolSubpage) 例如申请 1 KB 时,会进入对应规格的 Subpage。可以把 Subpage 看作装有多个同尺寸格子的零件盒:位图中的0表示空闲格子,1表示已分配格子。若现有零件盒均已满,Arena 会从 PoolChunk 中取得新的 Page,并将其初始化为该规格的零件盒。 - 第四站:巨型货架与空间规划(
PoolChunk) 当请求较大,例如 32 KB,零件盒不再适用。Arena 会在 PoolChunk 中按 Page 或连续 Page 组成的 run 查找连续空间。可以把这一过程近似理解为依据货架的空间规划图寻找合适的连续位置;具体的数据结构和切分策略以当前 Netty 版本实现为准。 - 第五站:补充新的货架(新的
PoolChunk) 当某个 Arena 中现有货架都无法满足请求时,该 Arena 会创建或获取新的 PoolChunk 后继续分配。PooledByteBufAllocator负责组织整套仓储体系,具体的补充与分配由 Arena 执行。
4.4 直接内存分配路径
对于 AbstractByteBufAllocator.directBuffer -> PooledByteBufAllocator.newDirectBuffer 的初始化路径:
java
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
// 事件循环线程通常会长期复用,因此其 PoolThreadCache 也能在该线程生命周期内持续复用。
// 是否实际启用线程缓存,仍取决于线程类型和 allocator 配置。
PoolThreadCache cache = threadCache.get();
// 获取该线程关联的直接内存 Arena,相当于找到负责直接内存的分区仓库。
PoolArena<ByteBuffer> directArena = cache.directArena;
final AbstractByteBuf buf;
if (directArena != null) {
// 通过直接内存 Arena 分配 ByteBuf;它会优先尝试当前线程的缓存。
buf = directArena.allocate(cache, initialCapacity, maxCapacity);
} else {
buf = PlatformDependent.hasUnsafe() ?
UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity) :
new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
onAllocateBuffer(buf, false, false);
}
return toLeakAwareBuffer(buf);
}
4.5 池化 ByteBuf 对象的获取
接下来分析 PoolArena.allocate 方法:
java
PooledByteBuf<T> allocate(PoolThreadCache cache, int reqCapacity, int maxCapacity) {
PooledByteBuf<T> buf = newByteBuf(maxCapacity);
allocate(cache, buf, reqCapacity);
return buf;
}
newByteBuf 最终会调用 PooledUnsafeDirectByteBuf.newInstance:
java
private static final Recycler<PooledUnsafeDirectByteBuf> RECYCLER =
new Recycler<PooledUnsafeDirectByteBuf>() {
@Override
protected PooledUnsafeDirectByteBuf newObject(Handle<PooledUnsafeDirectByteBuf> handle) {
return new PooledUnsafeDirectByteBuf(handle, 0);
}
};
static PooledUnsafeDirectByteBuf newInstance(int maxCapacity) {
// 通过对象池获取 ByteBuf 对象;没有可复用对象时才会创建新对象。
PooledUnsafeDirectByteBuf buf = RECYCLER.get();
// 将对象内部状态重置为初始状态,使其可以立刻作为新的 ByteBuf 使用。
buf.reuse(maxCapacity);
return buf;
}
RECYCLER.get() 会优先获取可复用的 PooledUnsafeDirectByteBuf;只有缓存未命中时,才会通过 newObject 创建对象。Recycler 负责复用 ByteBuf 对象本身,而底层内存块由 Arena、Chunk 和线程缓存协同管理。
4.6 按规格分配底层内存
继续分析 PoolArena.allocate 的实际分配逻辑:
java
private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
final int sizeIdx = sizeClass.size2SizeIdx(reqCapacity);
if (sizeIdx <= sizeClass.smallMaxSizeIdx) {
tcacheAllocateSmall(cache, buf, reqCapacity, sizeIdx);
} else if (sizeIdx < sizeClass.nSizes) {
tcacheAllocateNormal(cache, buf, reqCapacity, sizeIdx);
} else {
int normCapacity = sizeClass.directMemoryCacheAlignment > 0
? sizeClass.normalizeSize(reqCapacity) : reqCapacity;
// Huge allocations are never served via the cache so just call allocateHuge
allocateHuge(buf, normCapacity);
}
}
该方法会根据请求容量对应的 sizeIdx 选择不同的路径:Small 和 Normal 规格优先尝试线程缓存及 Arena,Huge 规格不经过线程缓存,直接走 allocateHuge。
写入数据的方法与前文的 ByteBuf 写入流程一致,此处不再展开。
4.7 内存归还与对象回收
下面看一下 release 方法的调用链:
SimpleLeakAwareByteBuf.release -> WrappedByteBuf.release -> AbstractReferenceCountedByteBuf.release -> AbstractReferenceCountedByteBuf.handleRelease -> PooledByteBuf.deallocate
重点查看 PooledByteBuf.deallocate:
java
protected final void deallocate() {
// handle >= 0 表示当前 ByteBuf 仍关联着一块已分配的池化内存区域。
if (handle >= 0) {
// 在归还内存前,通知分配器更新监控指标。
PooledByteBufAllocator.onDeallocateBuffer(this);
// 保存当前内存句柄,后续需要将这块内存归还给所属 Arena。
final long handle = this.handle;
// 先将 handle 设为 -1、memory 设为 null,解除当前 ByteBuf 与这块内存的关联。
this.handle = -1;
memory = null;
// 归还内存
chunk.arena.free(chunk, tmpNioBuf, handle, maxLength, cache);
// 清理临时引用,避免对象继续持有 Chunk、线程缓存和 NIO Buffer。
tmpNioBuf = null;
chunk = null;
cache = null;
// 这里只是将 ByteBuf 对象交还给 Recycler,供后续复用;并不等同于立刻回收 Java 对象。
this.recyclerHandle.unguardedRecycle(this);
}
}
随后进入对象回收链:Recycler.unguardedRecycle -> Recycler.DefaultHandle.recycle -> Recycler.LocalPool.release。
java
/**
* 释放句柄回池。
*
* @param handle 要释放的句柄
*/
protected final void release(H handle) {
Thread owner = this.owner;
if (owner != null && Thread.currentThread() == owner && batchSize < batch.length) {
// 同一线程,放入 batch 数组供下一次使用
batch[batchSize] = handle;
batchSize++;
} else if (owner != null && isTerminated(owner)) {
// 线程已终止,清理池
pooledHandles = null;
this.owner = null;
} else {
// 不同线程,放入共享队列
MessagePassingQueue<H> handles = pooledHandles;
if (handles != null) {
handles.relaxedOffer(handle);
}
}
}
该方法根据当前线程与对象池所属线程的关系选择回收位置:同线程时优先放入本地 batch,线程已终止时清理本地池,跨线程时放入共享队列。
在本示例中,代码运行在同一个 FastThreadLocalThread 中,并且本地池容量允许复用,因此第二次分配得到的底层 PooledByteBuf 可以与第一次相同。泄漏检测包装器本身未必复用,所以应通过 unwrap() 后的对象身份验证池化对象是否复用。