JVM如何管理直接内存?
⚙️ 一、直接内存的分配
- 通过
Unsafe
类分配- 直接内存的分配由
java.nio.ByteBuffer.allocateDirect()
触发,内部调用Unsafe.allocateMemory(size)
向操作系统申请堆外内存,返回内存地址。 - 分配时通过
Bits.reserveMemory()
检查是否超出-XX:MaxDirectMemorySize
限制(默认与堆最大值-Xmx
相同),若超出则抛出OutOfMemoryError
。
- 直接内存的分配由
- 创建
DirectByteBuffer
对象- 堆外内存地址被封装到堆内的
DirectByteBuffer
对象中,同时注册Cleaner(虚引用)用于后续回收。
- 堆外内存地址被封装到堆内的
🔄 二、直接内存的回收机制
- 自动回收流程
- 步骤1:当
DirectByteBuffer
对象不再被引用时,JVM垃圾回收器(GC)通过可达性分析判定其不可达,触发对象回收。 - 步骤2:对象回收时,关联的
Cleaner
(虚引用)被加入引用队列(Reference Queue)。 - 步骤3:后台线程ReferenceHandler(最高优先级守护线程)持续轮询引用队列,调用
Cleaner.clean()
方法。 - 步骤4:
Cleaner
触发Deallocator.run()
,调用Unsafe.freeMemory(address)
释放堆外内存。
- 步骤1:当
- 手动回收建议
- 若需立即释放,可显式调用
((DirectBuffer) buffer).cleaner().clean()
。 - 调用
System.gc()
可能触发Full GC,但不推荐(可能导致STW延迟),尤其在禁用-XX:+DisableExplicitGC
时无效。
- 若需立即释放,可显式调用
⚠️ 三、关键风险与管理策略
- 内存泄漏风险
- 原因:若
DirectByteBuffer
对象未被及时回收(如长期存活或循环引用),或禁用System.gc()
,可能导致Cleaner
无法触发,直接内存无法释放。 - 排查工具:
jcmd VM.native_memory
:查看堆外内存使用概况。Arthas memory
命令:监控直接内存池状态。
- 原因:若
- 参数调优
- 设置上限:
-XX:MaxDirectMemorySize=512m
避免超出物理内存。 - 避免禁用显式GC:除非高并发场景需减少Full GC,否则保留
-XX:-DisableExplicitGC
以支持自动回收。
- 设置上限:
- 替代方案
- 内存池复用:使用
ByteBuffer
池(如Netty的ByteBuf
)减少频繁分配开销。 - 软引用缓存:对非关键直接内存使用
SoftReference
,内存不足时自动回收。
- 内存池复用:使用
💎 四、总结:直接内存管理核心流程
分配:Unsafe.allocateMemory() → 创建DirectByteBuffer + 注册Cleaner(虚引用)
回收:GC回收对象 → Cleaner入队 → ReferenceHandler触发Cleaner.clean() → Unsafe.freeMemory()
- 关键依赖:虚引用 +
ReferenceHandler
线程 +Unsafe
原生操作。 - 最佳实践:
- 高频I/O场景使用直接内存,但需监控
MaxDirectMemorySize
。 - 避免长期持有
DirectByteBuffer
引用,防止内存泄漏。
- 高频I/O场景使用直接内存,但需监控