直接内存不属于jvm堆,无gc兜底,由-xx:maxdirectmemorysize管控;nio频繁分配未释放或cleaner延迟触发会导致outofmemoryerror: direct buffer memory。

直接内存不是堆内存,但OutOfMemoryError: Direct buffer memory真会炸
它压根不属于JVM运行时数据区——没有GC自动兜底,不走-Xmx限制,而是靠-XX:MaxDirectMemorySize单独管。默认值在JDK 8及以前等于-Xmx,但JDK 9+起默认为0(即不限,实际受限于物理内存和OS)。一旦NIO频繁分配ByteBuffer.allocateDirect()又没及时释放,或者GC迟迟不回收DirectByteBuffer对象,就直接抛Direct buffer memory。
- 现象:应用跑着跑着突然OOM,堆dump里对象不多,
jcmd <pid> VM.native_memory summary</pid>却显示Internal或Other区域飙升 - 本质:堆里的
DirectByteBuffer只是个“壳”,真正内存在OS那头;它靠Cleaner(虚引用)触发Unsafe.freeMemory(),但这个过程不保证及时 - 风险点:禁用
System.gc()(比如加了-XX:+DisableExplicitGC),或大量DirectByteBuffer长期存活在老年代,Cleaner根本等不到回收时机
ByteBuffer.allocateDirect()怎么用才不翻车
别把它当普通ByteBuffer随便new——分配成本高(系统调用malloc),回收不可控,必须池化复用。
- 禁止在循环里写
ByteBuffer.allocateDirect(1024):一次分配≈一次syscall,性能断崖下跌 - 推荐用Netty的
PooledByteBufAllocator或自建缓冲池,确保buffer.release()后归还,而非依赖GC - 如果必须手动控制,可用
((DirectBuffer) buffer).cleaner().clean()强制释放,但仅限确定该buffer已彻底不用,且没被其他线程持有 - 调试时加
-XX:NativeMemoryTracking=detail,再用jcmd <pid> VM.native_memory detail</pid>查哪块直接内存涨得最猛
零拷贝快在哪?关键就看FileChannel.transferTo()和SocketChannel.write()
传统BIO读文件:磁盘→内核缓冲区→JVM堆→再copy到socket缓冲区,CPU搬两次;NIO用直接内存后,只要底层支持(Linux sendfile、splice),就能让网卡直接从内核缓冲区取数据,跳过JVM堆这层——这才是“零拷贝”的真实含义。
- 生效前提:必须用
DirectByteBuffer,且I/O通道是FileChannel或SocketChannel等支持transfer的类型 - 典型高效写法:
fileChannel.transferTo(position, count, socketChannel),全程不进Java堆,连ByteBuffer都不需要显式创建 - 注意陷阱:Windows下
transferTo对非socket目标可能退化为普通copy;Android或某些容器环境也可能不支持 - 验证方式:用
perf record -e syscalls:sys_enter_write,syscalls:sys_enter_sendfile看系统调用次数是否显著减少
-XX:MaxDirectMemorySize设多少合适?别拍脑袋
设太小:DirectByteBuffer分配直接失败,抛OOM;设太大:OS物理内存耗尽,影响整个机器,甚至触发OOM Killer杀进程。
- 生产建议:初始值设为堆内存的50%~75%,例如
-Xmx4g -XX:MaxDirectMemorySize=3g;若用Netty,默认池大小是堆的50%,可据此反推 - 监控重点:不是看JVM堆使用率,而是用
cat /proc/<pid>/status | grep VmData</pid>(含直接内存)或NativeMemoryTracking输出里的Internal项 - 容器场景特别注意:Kubernetes里
memory.limit限制的是RSS总和,直接内存算在里面;若只配-Xmx不配MaxDirectMemorySize,JVM可能悄悄吃光剩余内存 - 上线前必做:用
stress-ng --vm 1 --vm-bytes 2G之类工具模拟内存压力,确认OOM行为符合预期
直接内存的麻烦不在分配,而在“看不见”——它不进堆dump,不响应jmap,连VisualVM都看不到具体对象。排查时最容易卡在以为是堆问题,结果Native Memory早爆了。







