directbytebuffer 通过 unsafe.allocatememory() 分配堆外内存,不由 jvm 垃圾回收管理,与堆内存本质区别在于地址稳定、支持 dma 和零拷贝;需用 -xx:maxdirectmemorysize 限制,否则易 oom。

DirectByteBuffer 是怎么分配的,和堆内存有啥本质区别
Java 的直接内存不是 JVM 堆的一部分,它由操作系统本地内存(native memory)提供,由 DirectByteBuffer 管理。JVM 只负责维护一个 Java 对象头(约 24 字节),真正数据存放在堆外 —— 这意味着 GC 不扫描、不移动、不回收那块内存。
分配靠的是 Unsafe.allocateMemory(),不是 new byte[]。所以即使堆足够大,-Xmx 也管不到它;必须用 -XX:MaxDirectMemorySize 单独限制,否则容易 OOM:错误信息是 java.lang.OutOfMemoryError: Direct buffer memory。
- 创建方式:用
ByteBuffer.allocateDirect(1024),底层调Unsafe.allocateMemory()+Unsafe.setMemory()清零 - 释放时机:依赖
Cleaner(JDK9+)或sun.misc.Cleaner(JDK8),在DirectByteBuffer被 GC 后异步触发freeMemory() - 不能手动调
freeMemory(),也不该用System.gc()强制催它 —— Cleaner 执行时机不可控,且频繁 GC 反而拖慢吞吐
为什么 NIO 零拷贝依赖直接内存
零拷贝(如 FileChannel.transferTo())的关键在于“让内核自己搬运”,避免数据在用户态内存和内核态缓冲区之间来回拷贝。但前提是用户态提供的缓冲区地址必须是物理连续、DMA 可见的 —— 堆内存不行,因为 JVM 堆随时可能被 GC 压缩移动,地址不固定。
DirectByteBuffer 分配的内存由 OS mmap 或 malloc 返回,地址稳定、支持 pinning(锁页),内核可直接读写。所以 transferTo() 底层调 sendfile()(Linux)或 TransmitFile()(Windows)时,传的就是这块地址。
- 如果传的是堆内
ByteBuffer,transferTo()会退化为普通 read/write,丧失零拷贝意义 -
SocketChannel.write()接收直接缓冲区时,同样跳过 copy 到内核 socket buffer 的步骤;堆内 buffer 则必须先拷一份过去 - 注意:
transferTo()在 Linux 上要求源 channel 是FileChannel,且目标 channel 必须支持 socket 类型,否则抛IOException
常见误用:把 DirectByteBuffer 当普通 buffer 用
很多人以为“直接内存更快,那就全换成 allocateDirect”,结果发现吞吐没涨,GC 次数变少但应用卡顿更明显 —— 因为直接内存分配/释放成本远高于堆内存,且不受 GC 精细调控。
Java Maven 项目审查工作流的通用输入处理。当 Java Maven 项目以 ZIP压缩包或 GitLab 仓库 URL 形式提供且需要...时使用。
典型误用场景:小对象高频创建(比如每次 HTTP 请求 new 一个 256B 的 DirectByteBuffer),导致 native memory 碎片化 + Cleaner 队列积压,最终触发 full GC 来逼 Cleaner 执行(JDK8 行为)。
- 推荐复用:用
ByteBufferPool管理直接缓冲区,避免反复 allocate/free - 别对小数据(
- JDK17+ 提供了
MemorySegment(来自 Panama 项目),比DirectByteBuffer更轻量、更可控,但尚未完全替代
怎么监控和排查直接内存泄漏
最直接的方式是看 NIO Buffer 相关指标:JVM 自带的 BufferPoolMXBean 暴露了当前 direct buffer 的总容量和 count,可通过 JConsole 或 jstat -gc <pid></pid> 查看 CCSC(Compressed Class Space)之外的 native 内存趋势。
更准的方法是加 JVM 参数:-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy,配合观察是否频繁出现 “Full GC (Ergonomics)” 且伴随 Direct buffer memory OOM —— 很可能是 Cleaner 积压或未及时清理。
- 用
jmap -histo:live <pid></pid>查java.nio.DirectByteBuffer实例数,暴增就是线索 - 开启 Native Memory Tracking(NMT):
-XX:NativeMemoryTracking=detail,再用jcmd <pid> VM.native_memory summary</pid>定位 direct memory 分配热点 - 注意:NMT 本身有 5–10% 性能开销,生产慎开;且 JDK8 的 NMT 不统计 Cleaner 持有的内存,JDK11+ 才完整
真正难的不是定位,而是确认某个 DirectByteBuffer 为何没被回收 —— 它背后往往藏着一个被意外强引用的 channel、selector 或自定义 handler,得顺着 Cleaner 的 referent 反向查引用链。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










