1. 概述
Java NIO 的 DirectByteBuffer 允许在堆外分配内存(直接内存),避免数据在堆内和堆外之间拷贝。但这种内存不受 JVM 堆管理,必须显式释放。JDK 利用虚引用(PhantomReference) + Cleaner + pending 链表 + ReferenceHandler 线程 实现了自动回收。
核心目标:当 DirectByteBuffer 对象变成垃圾时,自动释放其关联的堆外内存。
2. 核心组件与角色
| 组件 | 类型 | 职责 |
|---|---|---|
DirectByteBuffer | 堆内对象 | 持有堆外内存地址 address,并强引用一个 Cleaner |
Cleaner | PhantomReference 子类 | 虚引用监控 DirectByteBuffer,携带清理任务 Deallocator |
pending 链表 | JVM 内部静态结构 | 暂存 GC 发现的“需要后处理的引用对象” |
ReferenceHandler | JVM 守护线程 | 从 pending 取出引用对象,对 Cleaner 直接调用 clean() |
Deallocator | Runnable | 实际执行 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();
}
}
- 继承
PhantomReference,get()永远返回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);
}
}
}
pending是Reference类的静态字段,由 GC 填充。ReferenceHandler是 JVM 启动时创建的高优先级守护线程。
5.5 GC 如何将 Cleaner 放入 pending?(概念)
在 GC 的标记阶段(以 HotSpot 为例):
- 遍历所有
Reference对象,检查其referent是否仍然存活。 - 若
referent已不可达,且该引用满足处理条件(例如虚引用),则将该Reference对象链接到全局pending链表。 Cleaner作为PhantomReference的子类,同样遵循此规则。
6. 为什么使用虚引用而不是 finalize()?
| 特性 | finalize() | Cleaner(虚引用) |
|---|---|---|
| 调用时机 | 不可预测,依赖 GC | GC 标记后立即入 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引用链。
- NMT(
- 根本原因:
DirectByteBuffer对象被某个强引用意外持有(如集合、ThreadLocal),导致Cleaner无法被 GC 处理。
8. 总结
DirectByteBuffer 的堆外内存自动回收依赖于四个核心要素的协作:
Cleaner(虚引用):监控DirectByteBuffer的死亡,并持有清理任务。- GC:在标记阶段将不可达对象的
Cleaner挂入pending链表。 pending链表:GC 与ReferenceHandler之间的中转站。ReferenceHandler线程:从pending取出Cleaner,直接调用clean()释放内存。
这套机制实现了安全、及时、无复活风险的堆外内存自动回收,是 finalize() 的现代化替代方案。