靠默认配置撑不住极高并发,必须调参+严格释放+实时监控三者缺一不可,否则直接内存RSS会阶梯式上涨、抖动加剧甚至OOM;需调整maxCachedBufferCapacity、normalCacheSize、numDirectArenas三参数,严守release契约,并通过NMT和PoolArenaMetric监控原生内存。

靠默认配置撑不住极高并发,必须调参 + 严格释放 + 实时监控三者缺一不可,否则直接内存 RSS 会阶梯式上涨、抖动加剧,甚至触发 OutOfMemoryError: Direct buffer memory。
为什么 PooledByteBufAllocator 默认参数在高并发下会失效
Netty 4.1+ 虽默认启用池化,但其参数是按中低负载设计的。当连接数 > 10w、QPS > 5w、平均包长 > 2KB 时,常见现象包括:
-
jstat -gc看不到 GC 压力,但top显示 RSS 持续飙升(因为堆外内存不走 JVM GC) - 频繁抛
OutOfMemoryError: Direct buffer memory,即使-XX:MaxDirectMemorySize=4g也拦不住 - Metaspace 或 Compressed Class Space 异常增长(说明
ByteBuf对象创建/销毁频率过高)
根本原因不是“没用池化”,而是线程本地缓存对大 buffer 不生效、PoolArena 锁竞争加剧、以及 Cleaner 回收延迟叠加导致的碎片与泄漏。
必须调整的三个 JVM 启动参数
这三个参数直接影响 PooledDirectByteBuf 的复用率和碎片控制,不能只依赖 PooledByteBufAllocator.DEFAULT:
-
io.netty.allocator.maxCachedBufferCapacity=65536:默认 32768(32KB),意味着 ≥32KB 的 buffer 全部绕过线程本地缓存,每次都要抢PoolArena锁。设为 65536(64KB)可覆盖绝大多数 HTTP body / Protobuf 序列化结果 -
io.netty.allocator.normalCacheSize=64:默认 32,指每个规格(如 8KB、16KB)在线程缓存中最多存 32 个。高并发下易击穿,翻倍后能显著降低锁争用 -
io.netty.allocator.numDirectArenas=16:默认为 CPU 核数 × 2(16 核机器默认 32)。实测高并发短连接场景下,Arena 过多反而增加管理开销;16~24 更稳,需配合top -H -p $PID观察线程数与锁竞争
启动时加即可:-Dio.netty.allocator.maxCachedBufferCapacity=65536 -Dio.netty.allocator.normalCacheSize=64 -Dio.netty.allocator.numDirectArenas=16
业务代码中 release() 必须写在 handler 链末尾
ByteBuf 的引用计数不是可选项,是内存平滑的底线。常见漏释放场景:
-
channelRead()中拿到buf后,未在ctx.fireChannelRead()之后显式调用buf.release() - 跨线程投递(如
ctx.executor().execute(() -> {...}))后,在新线程里忘了release() - 异常分支未包裹
try-finally,导致buf在 decode 失败时未释放
正确写法示例:
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
try {
// 处理逻辑
process(buf);
} finally {
buf.release(); // 必须放在这里
}
}
}
必须开启 NMT 和 PoolArenaMetric 监控原生内存
仅靠 JVM 堆监控完全无效,堆外内存泄漏必须从 OS 层抓起:
- JVM 启动加
-XX:NativeMemoryTracking=detail,用jcmd $PID VM.native_memory summary查看各区域真实占用 - 暴露
PooledByteBufAllocator.metric(),定期采集directArenas的numActiveAllocations、numActiveBytes、tinySubpagePools等指标 - 重点关注
PoolThreadCache的命中率(cacheMiss持续上升说明缓存容量或规格不匹配)
最危险的盲区是:你看到 ByteBuf 对象被 GC 了,但底层 DirectByteBuffer 的 native memory 并未归还——它卡在 PoolThreadCache 里没刷回 PoolArena,或者 PoolArena 本身已碎片化无法合并。这时候光看 GC 日志毫无意义。

















