bigywater.tech
Java · 249

Direct ByteBuffer 池:堆外缓冲如何复用

direct buffer 在堆外,分配贵、回收靠 GC,常被池化复用。这篇用 BufferPoolMXBean、基准和 Netty 的池化分配器,逐项量出它的内存位置、分配成本、池化的收益,以及 MaxDirectMemorySize 与 DisableExplicitGC 的冲突。

52 vs 1640 ns
16 KB direct 缓冲区:Netty 池化 / JDK allocateDirect
第 65 个
MaxDirectMemorySize=64m + DisableExplicitGC 时抛 OOM 的位置
45249 → ~1 ns
1 MB:allocateDirect → 复用池子里的 buffer

堆外内存与 GC

实测 · 本机执行输出
  起始:count=0,capacity=0 MB,used=0 MB
  分配 100 个 1 MB 的 direct buffer 并持有:count=100,capacity=100 MB,used=100 MB
  (这些 buffer 在 Java 堆里的占用很小:堆占用约 3 MB)
  丢掉所有引用,但还没有 GC:count=100,capacity=100 MB,used=100 MB
  GC 之后:count=0,capacity=0 MB,used=0 MB
  allocateDirect 得到的内容全是 0:true;isDirect = true,hasArray = false,order = BIG_ENDIAN
  slice() 与原 buffer 共享同一块内存,不增加 direct 计数:count=1,capacity=1 MB,used=1 MB;slice.isDirect = true

分配成本

实测 · 本机执行输出
  大小    1024 字节:allocateDirect      241 ns/次;allocate(堆)     120 ns/次;自己写的池子借还      1 ns/次(30000 次,5 轮取最好)
  大小   65536 字节:allocateDirect     3401 ns/次;allocate(堆)    1143 ns/次;自己写的池子借还      1 ns/次(30000 次,5 轮取最好)
  大小 1048576 字节:allocateDirect    45249 ns/次;allocate(堆)   21712 ns/次;自己写的池子借还      1 ns/次(3000 次,5 轮取最好)

Netty 池化

实测 · 本机执行输出
  16 KB 的 direct 缓冲区 分配+写入+释放:Netty Pooled 52 ns/次;Netty Unpooled(release 立即释放)864 ns/次;JDK allocateDirect(靠 GC 回收)1640 ns/次
  Netty 池的指标:usedDirectMemory = 4096 KB,numDirectArenas = 10,chunkSize = 4096 KB,tinyCacheSize/smallCacheSize/normalCacheSize = 256/64
  持有 1000 个 16 KB 的池化 buffer:usedDirectMemory = 16 MB;JDK 的 BufferPoolMXBean(direct)= 117 MB
  全部 release 之后:usedDirectMemory = 4 MB(池里仍然保留着内存);JDK direct 容量 = 105 MB
  没有 release 的 ByteBuf 引用计数 refCnt = 1;release 一次后 refCnt = 0
  对已释放的 ByteBuf 再写 → IllegalReferenceCountException: refCnt: 0

MaxDirectMemorySize 与 System.gc()

实测 · 本机执行输出
  [-XX:MaxDirectMemorySize=64m,持有不放]
    持有 1 MB 的 direct buffer,第 65 个时 → OutOfMemoryError: Cannot reserve 1048576 bytes of direct buffer memory (allocated: 67108864, limit: 67108864)
  [-XX:MaxDirectMemorySize=64m,分配后立即丢弃]
    分配 2000 个 1 MB 并立即丢弃:全部成功,耗时 123 ms
  [同上,再加 -XX:+DisableExplicitGC]
    分配并立即丢弃,第 65 个时 → OutOfMemoryError: Cannot reserve 1048576 bytes of direct buffer memory (allocated: 67108864, limit: 67108864)
  [同上,再加 -Xlog:gc 看 GC 原因]
    [0.005s][info][gc] Using G1
    分配 2000 个 1 MB 并立即丢弃:全部成功,耗时 129 ms
    -Xlog:gc 记录的 GC 31 次;原因统计: {'System.gc()': 31, 'G1 Evacuation Pause': 0, 'G1 Humongous Allocation': 0}

分步演示

Direct ByteBuffer 池
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12(G1,-Xmx256m),Netty 4.1.115
    • 没有验证:Netty 的 noCleaner 与 -Dio.netty.maxDirectMemory、泄漏检测器(ResourceLeakDetector)、ZGC 下的 direct 内存回收、native 内存跟踪(NMT,见相关篇目)、分配成本里 JIT 与 GC 的干扰(这里取 5 轮最好)
    京ICP备2026067502号-1京公网安备11010502063487号