java堆外内存由jvm直接向os申请,不受-xmx限制但受-xx:maxdirectmemorysize约束,分配靠bytebuffer.allocatedirect(),回收依赖cleaner被动触发,易因gc延迟导致oom;需代码池化、配置合理、监控告警协同调优。

Java堆外内存(Direct Memory)不是JVM堆的一部分,而是由JVM向操作系统直接申请的内存区域,生命周期独立于GC,因此性能高但风险也高——用得好能大幅降低GC压力,用不好容易OOM或泄漏。
堆外内存怎么分配和回收?
核心是ByteBuffer.allocateDirect(),背后调用Unsafe.allocateMemory()向OS申请内存;同时JVM会为每个DirectByteBuffer关联一个Cleaner对象,它在GC发现该Buffer不可达时触发free()释放内存。但这依赖GC时机,属于“被动回收”,不是即时释放。
- 分配:调用
allocateDirect(n)即刻向OS申请n字节连续内存,不经过堆 - 回收路径有两条:显式调用
cleaner.clean()(不推荐手动触发),或等待GC后Cleaner线程异步执行Deallocator - 注意:
Cleaner本身是弱引用+虚引用机制,若GC长期不发生(如老年代几乎不动),堆外内存就卡住不释放
为什么容易OOM?关键原因在这儿
堆外内存不受-Xmx限制,但受-XX:MaxDirectMemorySize约束,默认值等于-Xmx。如果没显式设置,而程序大量创建DirectByteBuffer(比如Netty每连接分配多个ByteBuf),又没及时释放,就会突破上限报OutOfMemoryError: Direct buffer memory。
- 典型诱因:未关闭Channel、未释放PooledByteBuf、缓存长期持有DirectBuffer引用
- 排查工具首选NMT(Native Memory Tracking):启动加
-XX:NativeMemoryTracking=detail,运行中用jcmd <pid> VM.native_memory summary</pid>查direct内存用量 - 配合
jmap -histo:live <pid></pid>看DirectByteBuffer实例数,再结合jstack定位谁在持有着
实战调优三板斧
不是设个参数就完事,得从代码、配置、监控三层协同。
-
代码层:优先用池化方案(如Netty的
PooledByteBufAllocator),避免频繁allocate/free;所有DirectBuffer使用完务必调用clear()或确保其作用域结束(尤其在try-with-resources中无法自动关闭,需显式release()) -
配置层:根据物理内存和应用负载设
-XX:MaxDirectMemorySize=1g(例如4G机器可设512M–1G),并确保-Xmx与之匹配,避免堆挤占系统内存导致Direct分配失败 -
监控层:在Prometheus+Grafana中接入NMT指标,或用JMX暴露
java.nio:type=BufferPool,name=direct的MemoryUsed和TotalCapacity,设置阈值告警(如>80%持续2分钟)
Netty和Guava Cache里的坑与解法
这两个高频场景最容易翻车。
- Netty默认用
Unpooled分配DirectBuffer时,每次read/write都新建Buffer,必须切换成PooledByteBufAllocator并合理配置arena数(一般设CPU核数×2) - Guava Cache若存
DirectByteBuffer,需自定义RemovalListener并在onRemoval里调用buffer.clear()或cleaner.clean()(注意并发安全);更稳妥做法是缓存堆内byte[],仅IO时转Direct - 别信“GC最终会回收”——高吞吐服务可能几小时都不触发Full GC,堆外内存早被耗尽
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











