DirectBuffer 在 IO 通道写入时更快,因其支持零拷贝:JVM 直接将物理内存地址传给内核;而 HeapBuffer 需先复制到临时 DirectBuffer,小数据时开销显著。

DirectBuffer 在网络传输和磁盘 IO 中性能更好,但仅当缓冲区被反复复用、且单次传输数据量较大时才明显;HeapBuffer 在小数据、短生命周期场景下更轻量、更安全。
为什么 DirectBuffer 在 IO 通道写入时更快?
关键在于零拷贝路径:当调用 channel.write(directBuffer) 时,JVM 可直接将 address 指向的物理内存地址传给内核,数据从用户态直接送入 socket 或文件的内核缓冲区;而 channel.write(heapBuffer) 必须先触发一次 JVM 内部的隐式复制——底层会临时分配一个 DirectByteBuffer,再把 heapBuffer.array() 的内容 System.arraycopy 过去,之后才走相同路径。
- 这个复制过程对小 buffer(如
allocate(64))开销占比极高,甚至可能慢 2–3 倍 - Netty 等框架默认读取用
allocateDirect,就是为避免每次 read 都触发该复制 - 注意:如果后续还要对 buffer 做频繁数组访问(如解析协议头),
heapBuffer.array()是直接引用,directBuffer.array()会抛UnsupportedOperationException
HeapBuffer 在什么磁盘 IO 场景下反而更合适?
当 buffer 生命周期极短、且不直接参与 FileChannel 传输时,例如:内存中拼装 JSON 后一次性写入文件、日志格式化后暂存再刷盘。这类操作本质是「CPU 密集型」而非「IO 密集型」。
-
ByteBuffer.allocate(4096)分配快,GC 可控,无MaxDirectMemorySize越界风险 - 若用
allocateDirect(4096)做这种临时缓存,反而因 JNI 分配慢 + 无法及时释放(依赖 GC 触发Cleaner)导致堆外内存缓慢泄漏 -
Files.write(path, bytes)这类高层 API 内部已优化,不会从你传入的byte[]构造HeapByteBuffer再转DirectBuffer,它走的是 native writev 路径,与 buffer 类型无关
如何快速判断当前 buffer 是 Direct 还是 Heap?
别依赖 instanceof 或类名字符串匹配,用标准 API:
-
buffer.isDirect()—— 返回true即为DirectByteBuffer -
buffer.hasArray()—— 对HeapByteBuffer返回true,DirectByteBuffer返回false - 错误写法:
buffer.getClass().getName().contains("Direct"),在某些 JDK 版本或自定义实现中不可靠
特别注意:通过 ByteBuffer.wrap(byte[]) 创建的一定是 HeapByteBuffer,哪怕原数组来自 Unsafe.allocateMemory;而 allocateDirect 创建的对象,其 array() 方法永远抛异常。
容易被忽略的资源泄漏点
DirectBuffer 的「间接性」常被低估:它的 Java 对象(DirectByteBuffer 实例)在堆上,受 GC 管理;但真正的内存块在堆外,只有当该对象被 GC 回收、且 Cleaner 执行后才会释放。这意味着:
- 如果 buffer 被长期强引用(比如放进静态
ConcurrentHashMap),堆外内存永不释放,OutOfMemoryError: Direct buffer memory会先于堆溢出发生 -
Buffer.clear()或rewind()不释放内存,只是重置指针 - JDK 9+ 可用
Buffer.clear()后手动调用cleaner.clean()(需反射获取),但生产环境不推荐——应优先设计复用池,而非手动清理
真正需要警惕的不是「怎么选」,而是「选了之后怎么活过一周」:DirectBuffer 的生命周期必须比使用它的 Channel 更长,且不能脱离作用域意外逃逸。


















