结论:靠默认配置撑不住超高吞吐,必须调参+复用+监控三位一体,否则内存占用会阶梯式冲高、抖动甚至OOM;需调整maxCachedBufferCapacity、normalCacheSize、numDirectArenas三参数,严格遵循release契约,并通过NMT和PoolArenaMetric监控原生内存。

直接说结论:靠默认配置撑不住超高吞吐,必须调参 + 复用 + 监控三位一体,否则内存占用会阶梯式冲高、抖动甚至 OOM。
为什么默认 PooledDirectByteBuf 在超高吞吐下会“飘”
Netty 默认的内存池(PooledByteBufAllocator)在 4.1+ 版本虽已启用池化,但其参数是面向通用场景设计的。当 QPS 超过 5w、单连接平均包长 > 2KB、连接数 > 10w 时,常见现象包括:
- 直接内存 RSS 持续上涨,
jstat -gc看不到对应变化(因为不在堆里) - 频繁触发
OutOfMemoryError: Direct buffer memory,即使-XX:MaxDirectMemorySize设到 4G - GC 日志中出现大量
Metaspace或Compressed Class Space增长(间接说明 ByteBuf 对象创建/回收压力大)
根本原因不是“没复用”,而是默认的缓存策略对大缓冲区失效、Arena 竞争加剧、以及 Cleaner 回收延迟叠加。
必须调整的三个核心参数
这些参数直接影响大流量下 DirectBuffer 的复用率和碎片控制,不能只依赖默认值:
-
io.netty.allocator.maxCachedBufferCapacity=65536:默认 32KB,意味着 ≥32KB 的 buffer 全部不进线程本地缓存,每次都要走PoolArena分配。设为 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观察线程数与锁竞争
启动时加 JVM 参数即可生效:-Dio.netty.allocator.maxCachedBufferCapacity=65536 -Dio.netty.allocator.normalCacheSize=64 -Dio.netty.allocator.numDirectArenas=16
业务代码中必须遵守的释放契约
Netty 的引用计数机制(retain()/release())不是可选项,是内存平滑的底线。常见错误包括:
- 在
channelRead()中拿到ByteBuf后,未在 handler 链末尾显式buf.release(),尤其跨线程投递(如丢进EventLoop.execute())后容易漏掉 - 使用
Unpooled.copiedBuffer()包装池化 buffer,导致底层原始 buffer 无法归还池 —— 改用Unpooled.wrappedBuffer(buf)并确保外层也release() - 在异常分支中忘记
release(),推荐用 try-with-resources(需继承ReferenceCounted的 wrapper)或 finally 块兜底
一个安全写法示例:
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
try {
// 处理逻辑
process(buf);
} finally {
if (buf.refCnt() > 0) buf.release(); // 防御性检查
}
}
}
监控与压测中容易被忽略的两个信号
光看 GC 和堆内存毫无意义。真正决定 DirectBuffer 是否“平滑”的指标藏在原生内存层面:
- 开启 NMT(Native Memory Tracking):
-XX:NativeMemoryTracking=detail,然后用jcmd $PID VM.native_memory summary scale=MB定期抓取,重点关注Internal和Other类别是否缓慢爬升 —— 这往往是PooledDirectByteBuf未归还或Chunk碎片堆积的信号 - 观察
PooledByteBufAllocator.metric()返回的directArenas中每个PoolArenaMetric的numChunkLists和numSmallSubpages:若某 arena 的chunkListMetrics中count持续 > 200,说明该 arena 已成为热点,需拆分或调低numDirectArenas
最危险的情况是:NMT 显示 direct memory 稳定在 3.2G,但 PoolArena 内部统计显示已分配 4.8G 缓冲区 —— 这说明有近 1.6G 的 buffer 正卡在引用计数未归零或线程缓存未 flush 的中间态,随时可能引发抖动。

















