DirectByteBuffer堆外内存的自动回收机制

0 阅读3分钟

1. 概述

Java NIO 的 DirectByteBuffer 允许在堆外分配内存(直接内存),避免数据在堆内和堆外之间拷贝。但这种内存不受 JVM 堆管理,必须显式释放。JDK 利用虚引用(PhantomReference + Cleaner + pending 链表 + ReferenceHandler 线程 实现了自动回收。

核心目标:当 DirectByteBuffer 对象变成垃圾时,自动释放其关联的堆外内存。


2. 核心组件与角色

组件类型职责
DirectByteBuffer堆内对象持有堆外内存地址 address,并强引用一个 Cleaner
CleanerPhantomReference 子类虚引用监控 DirectByteBuffer,携带清理任务 Deallocator
pending 链表JVM 内部静态结构暂存 GC 发现的“需要后处理的引用对象”
ReferenceHandlerJVM 守护线程pending 取出引用对象,对 Cleaner 直接调用 clean()
DeallocatorRunnable实际执行 Unsafe.freeMemory(address) 释放堆外内存

3. 关系图(结构交互)

flowchart TB
    subgraph Heap["Java 堆"]
        D["DirectByteBuffer"]
        C["Cleaner (PhantomReference)"]
    end

    subgraph JVMInternal["JVM 内部"]
        GC["GC 线程"]
        P["pending 链表"]
        RH["ReferenceHandler 线程"]
    end

    subgraph NativeMemory["本地内存"]
        NM["堆外内存"]
        T["Deallocator (Runnable)"]
    end

    D -->|"强引用"| C
    C -.->|"虚引用 (referent)"| D
    C -->|"持有"| T

    GC -->|"referent 不可达时挂入"| P
    P -->|"取出"| RH
    RH -->|"调用 clean()"| C
    T -->|"freeMemory"| NM

    %% 子图背景色(极浅莫兰迪)
    classDef subHeap fill:#eceff4,stroke:#8fa0b0,stroke-width:1.5px
    classDef subJVM fill:#eef4ed,stroke:#8aad8a,stroke-width:1.5px
    classDef subNative fill:#fef5e7,stroke:#c0a070,stroke-width:1.5px

    %% 节点样式
    classDef heapNode fill:#d8e1ec,stroke:#4a6a8a,stroke-width:1.5px,color:#2c3e50
    classDef jvmNode fill:#cde3cd,stroke:#3b7a5e,stroke-width:1.5px,color:#2c3e50
    classDef nativeNode fill:#f5e2c0,stroke:#aa7a3c,stroke-width:1.5px,color:#2c3e50

    class D,C heapNode
    class GC,P,RH jvmNode
    class NM,T nativeNode

    class Heap subHeap
    class JVMInternal subJVM
    class NativeMemory subNative

4. 完整时序图

sequenceDiagram
    participant User
    participant D as DirectByteBuffer
    participant C as Cleaner
    participant GC as GC 线程
    participant P as pending 链表
    participant RH as ReferenceHandler
    participant Native as 堆外内存

    User->>D: new DirectByteBuffer()
    D->>Native: Unsafe.allocateMemory()
    D->>C: cleaner = Cleaner.create(this, deallocator)
    C-->>D: 虚引用 referent = this
    User->>D: buffer = null (切断强引用)

    Note over D: 无外部强引用

    GC->>D: 可达性分析(虚引用被忽略)
    GC-->>GC: 判定 D 不可达
    GC->>C: 发现 D 关联的虚引用 C
    GC->>P: 将 C 链入 pending 链表

    loop 不断处理
        RH->>P: 取出下一个引用对象
        alt C 是 Cleaner 实例
            RH->>C: c.clean()
            C->>Native: deallocator.run() → freeMemory()
            Native-->>Native: 释放物理内存
        else 其他引用 (Soft/Weak)
            RH->>ReferenceQueue: 入队
        end
    end

    Note over D,C: D 和 C 后续被 GC 回收

5. 关键源码分析(基于 JDK 8)

5.1 DirectByteBuffer 构造函数

DirectByteBuffer(int cap) {
    // 分配堆外内存
    long base = unsafe.allocateMemory(cap);
    address = base;
    // 更新全局计数器
    Bits.reserveMemory(cap);
    // 创建 Cleaner,关联自身
    cleaner = Cleaner.create(this, new Deallocator(base, cap));
}
  • Cleaner.create(this, deallocator) 将当前 DirectByteBuffer 对象作为被监控的 referent
  • Deallocator 保存地址 base 和容量 cap

5.2 Cleaner 类(sun.misc.Cleaner

public class Cleaner extends PhantomReference<Object> {
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();
    private final Runnable thunk;

    private Cleaner(Object referent, Runnable thunk) {
        super(referent, dummyQueue);   // 虚引用 + 假队列
        this.thunk = thunk;
    }

    public static Cleaner create(Object ob, Runnable thunk) {
        return new Cleaner(ob, thunk);
    }

    public void clean() {
        if (thunk != null) thunk.run();
    }
}
  • 继承 PhantomReferenceget() 永远返回 null
  • 构造时传入 dummyQueue(永远不会被使用),表示不走常规的 ReferenceQueue 机制。
  • clean() 直接执行 thunk

5.3 Deallocator(清理任务)

private static class Deallocator implements Runnable {
    private long address;
    private int size;

    Deallocator(long address, int size) {
        this.address = address;
        this.size = size;
    }

    public void run() {
        if (address == 0) return;
        unsafe.freeMemory(address);
        address = 0;
        Bits.unreserveMemory(size);
    }
}
  • 调用 Unsafe.freeMemory 归还堆外内存给操作系统。
  • 更新 Bits 中的全局计数器。

5.4 ReferenceHandler 线程(java.lang.ref.Reference 内部)

static class ReferenceHandler extends Thread {
    ReferenceHandler(ThreadGroup g) { ... }

    public void run() {
        for (;;) {
            Reference r = pending.dequeue();   // 从 pending 链表取一个引用
            if (r instanceof Cleaner) {
                ((Cleaner) r).clean();         // 特殊处理:直接清理
                continue;
            }
            // 普通软/弱/虚引用:放入用户关联的 ReferenceQueue
            ReferenceQueue q = r.queue;
            if (q != ReferenceQueue.NULL) q.enqueue(r);
        }
    }
}
  • pendingReference 类的静态字段,由 GC 填充。
  • ReferenceHandler 是 JVM 启动时创建的高优先级守护线程。

5.5 GC 如何将 Cleaner 放入 pending?(概念)

在 GC 的标记阶段(以 HotSpot 为例):

  • 遍历所有 Reference 对象,检查其 referent 是否仍然存活。
  • referent 已不可达,且该引用满足处理条件(例如虚引用),则将该 Reference 对象链接到全局 pending 链表。
  • Cleaner 作为 PhantomReference 的子类,同样遵循此规则。

6. 为什么使用虚引用而不是 finalize()

特性finalize()Cleaner(虚引用)
调用时机不可预测,依赖 GCGC 标记后立即入 pending,快速处理
性能对象需两次 GC不额外延长生命周期
对象复活可以不可能(get() 为 null)
异常处理异常被吞没可捕获并记录
官方态度Java 9 废弃推荐替代

7. 常见问题与排查

  • 泄漏现象:进程 RSS 持续增长,-XX:MaxDirectMemorySize 限制后抛出 Direct buffer memory
  • 排查工具
    • NMT(-XX:NativeMemoryTracking=detail)观察 Internal 区域增长。
    • Netty 的 ResourceLeakDetector-Dio.netty.leakDetectionLevel=paranoid)。
    • 堆转储(jmap -dump:live)分析 DirectByteBuffer 引用链。
  • 根本原因DirectByteBuffer 对象被某个强引用意外持有(如集合、ThreadLocal),导致 Cleaner 无法被 GC 处理。

8. 总结

DirectByteBuffer 的堆外内存自动回收依赖于四个核心要素的协作:

  1. Cleaner(虚引用):监控 DirectByteBuffer 的死亡,并持有清理任务。
  2. GC:在标记阶段将不可达对象的 Cleaner 挂入 pending 链表。
  3. pending 链表:GC 与 ReferenceHandler 之间的中转站。
  4. ReferenceHandler 线程:从 pending 取出 Cleaner,直接调用 clean() 释放内存。

这套机制实现了安全、及时、无复活风险的堆外内存自动回收,是 finalize() 的现代化替代方案。