directbytebuffer 通过 unsafe.allocatememory 直接申请堆外内存,不经过 jvm 堆,不受 gc 自动管理,需依赖 cleaner 机制释放;未及时释放会导致 direct buffer memory oom。

DirectByteBuffer 是怎么申请本地内存的
它不走 JVM 堆,而是调用 Unsafe.allocateMemory 直接向操作系统要内存,所以不受 GC 管理,也不算在 -Xmx 里。但代价是:你得自己管释放,否则就是隐形内存泄漏。
常见错误现象:OutOfMemoryError: Direct buffer memory —— 不是因为堆满了,而是 MaxDirectMemorySize(默认等于 -Xmx)被耗尽了,且大量 DirectByteBuffer 实例没被及时回收。
- 必须显式调用
buffer.clear()或buffer = null不起作用,真正释放靠的是 Cleaner 机制,依赖 GC 触发 - 如果频繁创建大块 direct buffer(比如每次网络读写都 new 一个 1MB 的
ByteBuffer.allocateDirect(1024*1024)),GC 压力会陡增,Cleaner 队列可能积压 - 调试时可加 JVM 参数
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps,观察是否频繁出现 “Cleaner” 相关的 finalizer activity
为什么不能直接 new DirectByteBuffer()
它的构造函数是 package-private 的,JDK 不让你直接 new。所有标准途径都经过 ByteBuffer.allocateDirect(),这个方法内部会走 Bits.reserveMemory() 做配额检查,再委托给 Unsafe 分配。
使用场景:NIO 文件通道(FileChannel.map())、网络 socket 读写(SocketChannel.read(buffer))、Netty 的 PooledByteBufAllocator 底层等——这些地方需要零拷贝或避免堆内复制开销。
立即学习“Java免费学习笔记(深入)”;
-
allocateDirect(n)会尝试分配 n 字节,但实际分配的内存通常略大于 n(对齐填充,常见 64B 或页对齐) - 不同 JDK 版本对齐策略不同:JDK 8 默认按 64B 对齐;JDK 17+ 可能更激进,尤其在开启
-XX:+UseLargePages时 - 别试图反射绕过构造限制——
DirectByteBuffer(long, int)构造器虽存在,但跳过配额检查,容易触发OutOfMemoryError而不报具体原因
如何安全释放 DirectByteBuffer 占用的内存
不能靠等待 GC,也不能手动调 Cleaner.clean()(它已 deprecated)。正确做法是确保 buffer 对象尽早不可达,并主动触发 Cleaner 执行(仅限调试/紧急场景)。
性能影响:Cleaner 是基于 ReferenceQueue 的异步清理机制,如果 buffer 对象生命周期长(比如被缓存、被 long-lived 对象引用),对应内存就一直不归还。
- 最稳妥方式:用完立即置为
null,并避免在 long-lived 对象中长期持有DirectByteBuffer - 调试时可强制触发:先获取 buffer 的 cleaner(
((DirectBuffer) buffer).cleaner()),再调cleaner.clean()—— 但生产环境禁用,因可能破坏内部状态 - JDK 14+ 引入
Buffer.clear()不再清底层内存,只重置 position/limit;真正释放仍依赖 Cleaner,这点和 heap buffer 行为不一致,容易误判
DirectByteBuffer 和堆内 ByteBuffer 性能差异在哪
不是“一定更快”,而是“在特定路径下避免拷贝”。比如 SocketChannel.write(directBuffer) 可直接交由 OS sendfile 或 epoll 写入,而 heap buffer 必须先 copy 到一个临时 direct buffer 中(JDK 自动做,叫 “unfolding”),多一次 memcpy。
容易踩的坑:小数据量 + 高频分配时,direct buffer 的分配/释放开销远超 heap buffer,反而更慢。
- 典型阈值:单次传输 > 4KB 且 buffer 复用率低时,direct 更合适;反之 heap buffer 更轻量
- Netty 默认用池化 direct buffer,就是为摊平分配成本;自己手写 NIO 服务时若没池化,
allocateDirect调用本身就成了瓶颈 - 注意 native stack:某些 JNI 调用(如 OpenSSL)会把 direct buffer 地址传入 C 层,若此时 Java 层 buffer 已被 Cleaner 回收,C 层继续访问就会 crash —— 这类 bug 很难复现,但后果严重
真正的难点不在申请,而在生命周期管理。只要 buffer 还被某个线程栈、静态集合、甚至 ThreadLocal 持有,内存就不会还给 OS,而你从 jstat 或 VisualVM 里根本看不到这部分占用。










