直接内存与堆外内存
直接内存(Direct Memory)位于 JVM 堆之外,不受堆大小限制,但受进程可用内存与 -XX:MaxDirectMemorySize 约束。它绕过堆与内核之间的数据拷贝,是 NIO 高性能 IO 的基础。
为什么需要堆外内存
传统 IO 一次读写涉及两次拷贝:
磁盘/网卡 → 内核缓冲区 → 堆内 byte[](JNI 拷贝)NIO 直接缓冲区直接把数据放在堆外,省掉内核缓冲区与堆之间的拷贝,读写的字节数据不再在堆内和 native 内存间来回搬运。同时堆外内存不占用堆空间,可减少 GC 压力。
DirectByteBuffer 分配原理
ByteBuffer.allocateDirect(capacity) 是使用直接内存的标准入口:
java
import java.nio.ByteBuffer;
ByteBuffer buffer = ByteBuffer.allocateDirect(1024); // 堆外分配 1KB
buffer.putInt(42); // 写入
buffer.flip(); // 切换读模式
int v = buffer.getInt(); // 读出其背后的实现链路:
DirectByteBuffer构造时调用Unsafe.allocateMemory(size)申请堆外内存- 申请成功后,通过
Cleaner注册一个虚引用,用于内存回收 - 堆内只保留一个 8 字节的引用(address 字段),真正的数据在堆外
关键点:DirectByteBuffer 对象本身在堆内,但数据在堆外。若 DirectByteBuffer 对象被回收而 Cleaner 未及时执行,堆外内存可能延迟释放。
Unsafe 直接分配
java
import sun.misc.Unsafe;
import java.lang.reflect.Field;
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
long addr = unsafe.allocateMemory(1024); // 分配 1KB 堆外内存
unsafe.putLong(addr, 42L); // 写入
long v = unsafe.getLong(addr); // 读取
unsafe.freeMemory(addr); // 必须手动释放与 DirectByteBuffer 不同,Unsafe 分配的内存没有 Cleaner 自动回收,必须手动 freeMemory,否则必然泄漏。Netty 等框架基于 Unsafe 封装了 PooledDirectByteBuf,用内存池 + 引用计数管理堆外内存,避免频繁分配与释放。
堆外内存泄漏排查
堆外内存泄漏的特征:进程 RSS 持续增长,堆却一直平稳,GC 正常。
定位步骤
- 确认堆外增长:
top观察 RES;jstat -gc确认堆未涨;排除堆内 OOM - 确认 DirectBuffer 用量:观察
jmap -histo:live <pid> | head -20DirectByteBuffer、DirectByteBuffer$Deallocator数量是否异常增长 - 开启跟踪参数:
-XX:MaxDirectMemorySize=256m -XX:+DisableExplicitGC (谨慎,会禁用 System.gc 触发的 Cleaner) -XX:+PrintGCDetails - 使用 NMT(Native Memory Tracking):可以看到 Internal(Unsafe 分配)与 Other 的内存去向
-XX:NativeMemoryTracking=summary jcmd <pid> VM.native_memory summary - 堆外内存 dump:Linux 下用
gdb结合 jdk.nio 内部结构导出堆外内容分析(高阶手段,多数场景 NMT 足够)
常见泄漏源
| 场景 | 原因 |
|---|---|
| 大对象长期持有 DirectByteBuffer | Cleaner 依赖 GC,引用未断则无法释放 |
| Unsafe.allocateMemory 未 freeMemory | 无自动回收,纯手动管理 |
| 线程池中 ByteBuffer 不归还 | 池化资源未释放或引用被外部持有 |
使用 -XX:+DisableExplicitGC 且堆充足 | System.gc 被禁用,Cleaner 触发延迟 |
MaxDirectMemorySize 参数
-XX:MaxDirectMemorySize=size 控制直接内存上限,默认值等于 -Xmx(堆最大值)。需要注意:
- 只限制 DirectByteBuffer 的分配,不限制 Unsafe 手动分配
- 超限分配时,JDK 会尝试触发
System.gc()来执行 Cleaner 回收,若此时堆外占用仍高则抛OutOfMemoryError: Direct buffer memory - 该 OOM 不会表现为堆 OOM,进程内存却可能已接近物理上限
使用建议
- 大块、频繁的 IO 数据用 DirectByteBuffer;小块数据用堆内缓冲更划算
- 优先使用 Netty 等带内存池的框架,避免频繁
allocateDirect - 设置
MaxDirectMemorySize兜底,防止堆外无限增长打爆物理内存 - 不要滥用
-XX:+DisableExplicitGC,它会削弱堆外内存的自动回收能力