靠默认配置撑不住高吞吐,必须显式启用池化、严格管控引用计数、避免跨线程误释放;根本原因是混用Unpooled缓冲区或漏调release()导致Netty内存池失效,使DirectBuffer脱离池管理而依赖不可控的JVM Cleaner机制。

直接结论:靠默认配置撑不住高吞吐,必须显式启用池化 + 严格管控引用计数 + 避免跨线程误释放。
为什么 PooledUnsafeDirectByteBuf 的内存占用会“不平滑”
现象是:压测时 RSS 内存阶梯式上涨、Full GC 频发、甚至 OutOfMemoryError: Direct buffer memory;但堆内存曲线却很平稳。根本原因不是“用了 DirectBuffer”,而是没用对池化机制——比如混用了 Unpooled.directBuffer(),或 writeAndFlush() 后忘了 release(),又或者在非 I/O 线程里错误地调用了 retain()/release()。
Netty 的内存池(PooledByteBufAllocator)只管理它自己分配的缓冲区;一旦你绕过它手动 new 或用 unpooled 工厂,就等于把内存交还给 JVM 的 Cleaner 机制,释放延迟不可控,碎片无法回收。
必须显式配置并验证 PooledByteBufAllocator 是否生效
默认的 ChannelHandlerContext.alloc() 在某些旧版本 Netty 或自定义 Bootstrap 场景下可能 fallback 到 UnpooledByteBufAllocator,尤其当你没显式传入 allocator 时。
- 初始化 Bootstrap 时务必指定:
bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) - 检查日志是否出现
Using PooledUnsafeDirectByteBuf(开启-Dio.netty.leakDetection.level=advanced可辅助验证) - 避免在 Handler 中写
ctx.alloc().buffer()后再转成Unpooled.wrappedBuffer(...)—— 这会切断池化链路
引用计数管理是平滑内存占用的唯一开关
PooledUnsafeDirectByteBuf 的生命周期完全由 refCnt 控制:只有 refCnt 降为 0 时,缓冲区才真正归还内存池。任何一次漏掉 release(),都会导致该缓冲区永远滞留在池外,变成“伪泄漏”。
- 出站消息:若由业务线程创建(如响应组装),必须在
writeAndFlush()后立即release();Netty 不会帮你释放“你主动创建的”出站对象 - 入站消息:Netty 默认在 channelReadComplete 后自动
release(),但如果你在channelRead()中做了retain()(例如异步处理),就必须在最终消费完后配对release() - 切片操作(
slice()、readSlice())会共享底层内存,但 refCnt 不继承——需手动retain(),否则父缓冲区一 release,子 slice 就变 invalid
容易被忽略的三个硬约束点
即便 allocator 正确、refCnt 完整,以下三点仍会导致内存占用抖动或持续爬升:
-
PooledByteBufAllocator的 arena 数量默认是2 * Runtime.getRuntime().availableProcessors(),但在容器环境(如 CPU quota 限制)下可能误判,建议显式设为固定值,例如new PooledByteBufAllocator(true, 4, 4, 8192, 11, 0, 0, 0) - 小缓冲区(
- 监控不能只看堆内存:必须采集
PlatformDependent.usedDirectMemory()和BufferPoolMetric(如 Netty 4.1.100+ 提供的 JMX 指标),否则永远发现不了池内堆积











