bigywater.tech
Java · 162

ByteBuffer:堆内、堆外和 Direct Memory

ByteBuffer 的堆内、堆外到底有什么区别?堆外内存什么时候释放、上限在哪?这篇用 BufferPoolMXBean 和几段小程序量出来(flip/clear/compact 的位置语义已有专门文章)。

21 vs 100 µs
分配 1 MB 的堆内 / 堆外 buffer
10240 → 10240 → 0 KB
堆外内存:分配后、丢引用后、System.gc() 后
64 MB
MaxDirectMemorySize=64m 时能成功分配的堆外内存

基本差别

实测 · 本机执行输出
  堆内:isDirect = false,hasArray = true;堆外:isDirect = true,hasArray = false(堆外没有可访问的 byte[])
  默认字节序 = BIG_ENDIAN;putInt(1) 后第一个字节 = 0、最后一个字节 = 1;改成 LITTLE_ENDIAN 后重写:[1, 0, 0, 0]
  slice() 与原 buffer 共享内容:原 buffer 的第 0 个字节被 slice 改成了 99(写 slice 就是写原来的数据);position 与 limit 各自独立:原 0/8,slice 0/8
  asReadOnlyBuffer() 之后写 → ReadOnlyBufferException(但它仍然看得到原 buffer 的修改:ro.get(0) = 99)

分配与写文件

实测 · 本机执行输出
  分配 1 MB 的 buffer(第 3 轮,前两轮预热):堆内 21 µs/个(2000 个),堆外 100 µs/个(200 个)
  FileChannel 写 512 MB(每次 1 MB 堆内 buffer):103 ms
  FileChannel 写 512 MB(每次 1 MB 堆外 buffer):157 ms
  FileChannel 写 512 MB(每次 1 MB 堆内 buffer):175 ms
  FileChannel 写 512 MB(每次 1 MB 堆外 buffer):148 ms

释放时机

实测 · 本机执行输出
  起始:堆外已用 0 KB,个数 0
  分配 10 个 1 MB 堆外 buffer 后:堆外已用 10240 KB,个数 10(堆内存占用几乎不变:堆已用 3 MB)
  丢掉引用(keep.clear())之后:堆外已用 10240 KB(没有立刻释放)
  System.gc() 之后:堆外已用 0 KB,个数 0(靠 GC 触发清理)
  没有设上限时分配 100 MB 堆外:成功

堆外上限

实测 · 本机执行输出
  已成功分配 64 MB 后,再分配 1 MB → OutOfMemoryError: Cannot reserve 1048576 bytes of direct buffer memory (allocated: 67108864, limit: 67108864)
  此时堆内存几乎没用:堆已用 1 MB(-Xmx 是 256 MB)

分步演示

ByteBuffer
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12
    • 没有验证:真实磁盘或网络 I/O(本篇的写文件落在操作系统缓存里)、MappedByteBuffer 与 Cleaner 的细节、Unsafe.invokeCleaner、netty 的池化堆外内存、JDK 17 与 27 的差异;耗时数字波动明显
    京ICP备2026067502号-1京公网安备11010502063487号