ByteBuf内存分配
ByteBuf内存分配
对比表格:有/无release()的完整对比
1. 池化直接内存
有对象池、有内存池
ByteBuf buf = PooledByteBufAllocator.DEFAULT.directBuffer(1024)
| 场景 | ByteBuf对象本身 | ByteBuf引用的内存块 |
|---|---|---|
调用release() |
✅ 回收到对象池(复用) | ✅ 回收到内存池(复用) |
不调用release(),对象被GC |
✅ 被GC回收(变成垃圾) | ❌ 内存池标记"使用中",泄漏 |
不调用release(),对象没GC |
❌ 占用Java堆内存 | ❌ 内存池标记"使用中",泄漏 |
2. 非池化直接内存
无对象池、无内存池
ByteBuf buf = UnpooledByteBufAllocator.DEFAULT.directBuffer(1024)
| 场景 | ByteBuf对象本身 | ByteBuf引用的内存块 |
|---|---|---|
调用release() |
✅ 被GC回收(没有对象池) | ✅ 直接调用 free() 归还给OS |
不调用release(),对象被GC |
✅ 被GC回收 | ❌ 直接内存无法被GC,严重泄漏 |
不调用release(),对象没GC |
❌ 占用Java堆内存 | ❌ 直接内存无法被GC,严重泄漏 |
3. 池化堆内存
有对象池、有内存池
ByteBuf buf = PooledByteBufAllocator.DEFAULT.heapBuffer(1024)
| 场景 | ByteBuf对象本身 | ByteBuf引用的内存块(byte[]) |
|---|---|---|
调用release() |
✅ 回收到对象池(复用) | ✅ 回收到内存池(byte[]复用) |
不调用release(),对象被GC |
✅ 被GC回收(变成垃圾) | ⚠️ 被GC回收(因为byte[]在堆中),但内存池丢失了这个数组,池化失效 |
不调用release(),对象没GC |
❌ 占用Java堆内存 | ❌ 占用堆内存,且内存池无法复用 |
特别注意: 池化堆内存不调用
release()时,byte[]最终会被GC回收(因为它在堆中),不会造成传统意义上的内存泄漏。但是,内存池丢失了这块内存,导致池化失效,长期运行会影响性能。这也是为什么池化堆内存也必须调用release()!
4. 非池化堆内存
无对象池、无内存池
ByteBuf buf = UnpooledByteBufAllocator.DEFAULT.heapBuffer(1024)
| 场景 | ByteBuf对象本身 | ByteBuf引用的内存块(byte[]) |
|---|---|---|
调用release() |
✅ 被GC回收(没有对象池) | ✅ 失去引用,等待GC回收 |
不调用release(),对象被GC |
✅ 被GC回收 | ✅ 失去引用,等待GC回收 |
不调用release(),对象没GC |
❌ 占用Java堆内存 | ❌ 占用堆内存 |
关键洞察: 非池化堆内存是唯一不调用
release()也不会真正内存泄漏的情况(因为byte[]在堆中,GC能回收)。但是:
- 对象无法复用,频繁分配造成GC压力
- 如果对象被长期持有(如放在静态集合中),仍然会泄漏
四种方式全景对比
| 特性 | 池化直接内存 | 非池化直接内存 | 池化堆内存 | 非池化堆内存 |
|---|---|---|---|---|
| 对象池 | ✅ 有 | ❌ 无 | ✅ 有 | ❌ 无 |
| 内存池 | ✅ 有 | ❌ 无 | ✅ 有 | ❌ 无 |
| 不release会泄漏? | ✅ 严重泄漏 | ✅ 严重泄漏 | ⚠️ 池化失效(但GC能回收) | ❌ 不泄漏(但GC压力大) |
| 性能 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| GC压力 | 低 | 中 | 低 | 高 |
| 适用场景 | 高并发网络 | 低并发测试 | 高频小数据 | 简单脚本 |
PooledByteBufAllocator里面有4个池子,堆外对象池、堆外内存池、堆内对象池、堆内内存池。
PooledByteBufAllocator 中的 4 个池子对应字段:
// 在 `PooledByteBufAllocator` 中(顶层入口):
// 堆外 Arena 数组(堆外对象池 + 堆外内存池 的入口)
private final PoolArena<ByteBuffer>[] directArenas;
// 堆内 Arena 数组(堆内对象池 + 堆内内存池 的入口)
private final PoolArena<byte[]>[] heapArenas;
// 在 `PoolArena<T>` 中(每个 Arena 内部细分):
public abstract class PoolArena<T> {
// ========== 对象池(对象级别的缓存复用) 新版本在这里 ==========
// 1. 堆外对象池 或 堆内对象池
// 复用 ByteBuf 对象本身,避免频繁 new
private final ObjectPool<PooledByteBuf<T>> pooledByteBufPool;
// 或者使用 Recycler(Netty 的轻量级对象池)
// 具体通过 Recycler<PooledByteBuf<T>> 实现
// ========== 内存池(内存块的分配管理) ==========
// 2. 堆外内存池 或 堆内内存池
// 六种不同使用率的 PoolChunkList,按使用率从低到高排列
private final PoolChunkList<T> qInit; // [0, 25) 初始 chunk
private final PoolChunkList<T> q000; // [1, 50) 低使用率
private final PoolChunkList<T> q025; // [25, 75) 中低使用率
private final PoolChunkList<T> q050; // [50, 100) 中高使用率
private final PoolChunkList<T> q075; // [75, 100) 高使用率
private final PoolChunkList<T> q100; // [100, 100] 满 chunk
// 小内存分配使用 PoolSubpage(从 PoolChunk 中切分)
private final PoolSubpage<T>[] smallSubpagePools; // 按 2^n 分档
}
| 池子名称 | 字段位置 | 字段名 | 作用 |
|---|---|---|---|
| 堆外对象池 | PoolArena<ByteBuffer> |
Recycler<PooledByteBuf<T>>(老版本在PooledDirectByteBuf 类内部static Recycler |
复用 PooledDirectByteBuf 对象 |
| 堆外内存池 | PoolArena<ByteBuffer> |
PoolChunkList<T>[] + PoolSubpage<T>[] |
管理 Direct Memory 内存块分配 |
| 堆内对象池 | PoolArena<byte[]> |
Recycler<PooledByteBuf<T>>(老版本在PooledHeapByteBuf 类内部static Recycler |
复用 PooledHeapByteBuf 对象 |
| 堆内内存池 | PoolArena<byte[]> |
PoolChunkList<T>[] + PoolSubpage<T>[] |
管理 JVM Heap byte[] 内存块分配 |
关键点:directArenas 和 heapArenas 是顶层入口,每个 Arena 内部再分为对象池(复用 ByteBuf 对象本身)和内存池(管理实际内存块),合起来就是你所说的 4 个池子。
Netty线程级缓存
PooledByteBufAllocator
├── directArenas[] ← 堆外 Arena 数组(内存池)
├── heapArenas[] ← 堆内 Arena 数组(内存池)
│
└── PoolThreadLocalCache (FastThreadLocal) ← 只有一个 useCacheForAllThreads 字段
│ 只是用来为每个线程创建 PoolThreadCache
│
└── [per thread] PoolThreadCache ← 线程级缓存在这里!
├── heapArena ← 本线程绑定的 Arena 引用
├── directArena ← 本线程绑定的 Arena 引用
├── MemoryRegionCache[] × 6 ← 线程本地内存缓存
└── allocations ← 分配计数
Netty 线程(NioEventLoop)本身没有独立的对象池和内存池,但它通过 PoolThreadCache 绑定了一个 Arena 和线程级缓存,加速本线程的 ByteBuf 分配。再加上 Recycler 复用任务对象和 ByteBuf 实例,形成了一套完整的线程本地复用体系。
一个线程生成ByteBuf对象时,对象本身复用是从PooledDirectByteBuf内部的对象池里获取,内存是从线程自己的PoolThreadCache里面优先分配,PoolThreadCache里面的内存是关联PooledByteBufAllocator里面的内存池。
PoolThreadCache 的创建时机:懒加载,不是线程启动时就创建,而是该线程第一次调用 alloc.directBuffer() 或 alloc.heapBuffer() 分配 ByteBuf 时才创建。
总结:对象从
Recycler复用,内存从PoolThreadCache优先分配,PoolThreadCache背后挂着一个PoolArena(也就是PooledByteBufAllocator里的内存池)。三层各司其职,配合起来实现高性能内存管理。
面试常问的问题
Q1: 池化堆内存不调用release()会内存泄漏吗?
A: 不会造成传统意义上的内存泄漏(因为byte[]在堆中,GC能回收),但会导致池化失效,内存池不断"丢失"byte[],最终所有byte[]都变成普通对象,池化变成摆设,性能下降。
Q2: 那池化堆内存为什么还需要release()?
A: 为了归还对象到对象池和归还byte[]到内存池,实现真正的复用。如果不调用,池化机制失效,退化为非池化的性能。
Q3: 什么时候用堆内存,什么时候用直接内存?
A:
- 堆内存:小数据、频繁操作、内存敏感
- 直接内存:大数据、网络传输、零拷贝场景
Q4: 所有情况都必须调用release()吗?
A: 只有非池化堆内存可以偷懒(但仍不推荐),其他三种必须调用!最佳实践是无论何时都调用release()。
终极记忆总结
一句话总结: 只有非池化堆内存可以不release()(但会加大GC压力),其他三种必须release(),否则要么内存泄漏,要么池化失效!生产环境所有ByteBuf都要release。
浙公网安备 33010602011771号