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能回收)。但是:

  1. 对象无法复用,频繁分配造成GC压力
  2. 如果对象被长期持有(如放在静态集合中),仍然会泄漏

四种方式全景对比

特性 池化直接内存 非池化直接内存 池化堆内存 非池化堆内存
对象池 ✅ 有 ❌ 无 ✅ 有 ❌ 无
内存池 ✅ 有 ❌ 无 ✅ 有 ❌ 无
不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 RECYCLER 复用 PooledDirectByteBuf 对象
堆外内存池 PoolArena<ByteBuffer> PoolChunkList<T>[] + PoolSubpage<T>[] 管理 Direct Memory 内存块分配
堆内对象池 PoolArena<byte[]> Recycler<PooledByteBuf<T>>老版本在PooledHeapByteBuf 类内部static Recycler RECYCLER 复用 PooledHeapByteBuf 对象
堆内内存池 PoolArena<byte[]> PoolChunkList<T>[] + PoolSubpage<T>[] 管理 JVM Heap byte[] 内存块分配

关键点:directArenasheapArenas顶层入口,每个 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

posted @ 2026-07-05 20:26  deyang  阅读(12)  评论(0)    收藏  举报