# 6. JVM 堆外内存有什么用?该如何使用?

# 标准答案

JVM 堆外内存(Off-Heap Memory) 是指不受 JVM GC 直接管理的内存区域,通常使用 NIO ByteBufferUnsafe 以及 DirectByteBuffer 进行分配。堆外内存的主要作用是提高 I/O 访问效率,减少 GC 压力,优化大对象管理。正确使用堆外内存需要注意 释放时机、防止 OOM 以及优化性能,可以通过 DirectByteBuffersun.misc.UnsafeForeign Memory API(JDK 14+) 进行操作。

# 答案解析

# 1. 为什么需要堆外内存?

JVM 主要使用 堆内存(Heap Memory) 进行对象分配和管理,而堆外内存则绕过 GC,减少 GC 开销,提高性能。堆外内存的主要用途包括:

  • 高性能 I/O:Java NIO 的 DirectByteBuffer 绕过堆内存,直接与操作系统交互,提高 I/O 效率。
  • 减少 GC 影响:堆外内存不受 GC 控制,可以避免 GC 扫描和回收大对象,降低 GC 频率。
  • 大对象存储:堆外内存适用于 缓存、日志系统、大数据计算 等场景,如 Netty、Kafka、RocksDB 都大量使用堆外内存。
  • 共享内存:进程间可以共享堆外内存,避免频繁的数据复制,提升通信效率。

# 2. JVM 堆外内存的主要使用方式

JVM 提供了几种方式分配和管理堆外内存:

# (1)DirectByteBuffer(推荐方式)
  • DirectByteBuffer 是 Java NIO 提供的 直接缓冲区,绕过 JVM 堆内存,直接分配到操作系统的本地内存
  • 主要用于高性能网络通信(如 Netty)、大文件处理(如 MappedByteBuffer)
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024); // 直接分配1KB堆外内存
directBuffer.putInt(42);
int value = directBuffer.getInt(0);
1
2
3

注意:JVM 通过 Cleaner 机制自动释放 DirectByteBuffer,但回收不及时可能导致 OOM。

# (2)Unsafe.allocateMemory()(不安全,需手动释放)

sun.misc.Unsafe 可以直接分配和释放堆外内存,但容易导致内存泄漏JDK 11+ 受限制

Unsafe unsafe = getUnsafe();
long memoryAddress = unsafe.allocateMemory(1024); // 分配1KB堆外内存
unsafe.putLong(memoryAddress, 12345L);
long value = unsafe.getLong(memoryAddress);
unsafe.freeMemory(memoryAddress); // 手动释放
1
2
3
4
5

问题:需要手动 freeMemory(),否则会导致内存泄漏,不推荐在业务代码中使用。

# (3)Foreign Memory API(JDK 14+ 新方案)

Java 14 引入 Foreign Memory API(Project Panama),提供更安全的堆外内存管理方式。

try (MemorySegment segment = MemorySegment.allocateNative(1024)) {
    segment.set(ValueLayout.JAVA_LONG, 0, 12345L);
    long value = segment.get(ValueLayout.JAVA_LONG, 0);
}
1
2
3
4

优势:支持 自动释放,避免内存泄漏,适用于 高性能应用

# (4)MappedByteBuffer(适用于大文件映射)

JVM 提供 MappedByteBuffer 允许直接将文件映射到内存,适用于 日志存储、数据库索引 等场景。

FileChannel fileChannel = new RandomAccessFile("data.dat", "rw").getChannel();
MappedByteBuffer mappedBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
mappedBuffer.put(0, (byte) 42); // 直接修改文件内容
1
2
3

优势:文件修改直接反映到磁盘,适用于 大文件处理

# 3. 如何避免堆外内存 OOM?

堆外内存不受 JVM GC 控制,一旦泄漏可能导致 OOM,需要注意以下几点:

  • 设置 -XX:MaxDirectMemorySize 限制堆外内存大小(默认与 -Xmx 相同)。

    -XX:MaxDirectMemorySize=512M
    
    1

    限制 DirectByteBuffer 总大小,防止无限制分配导致 OOM。

  • 使用 Cleaner 及时释放内存

    ((DirectBuffer) directBuffer).cleaner().clean(); // 主动回收 DirectByteBuffer
    
    1
  • Netty、Kafka 采用对象池管理堆外内存

    • Netty PooledByteBufAllocator 避免频繁分配释放,减少内存碎片。
    • Kafka MemoryPool 通过 ReferenceCounted 管理缓冲区,防止泄漏。

# 4. JVM 参数调优

  • 限制 DirectByteBuffer 的最大堆外内存:
    -XX:MaxDirectMemorySize=1G
    
    1
  • 调整 -Xmx 避免和 -XX:MaxDirectMemorySize 争夺内存:
    -Xmx4G -XX:MaxDirectMemorySize=2G
    
    1
    避免 Java 堆和堆外内存争抢系统内存,导致 OOM。

# 5. 堆外内存的业务场景

  • Netty、Kafka、RocketMQ:使用 DirectByteBuffer 减少数据拷贝,提高 I/O 性能。
  • RocksDB、ClickHouse:使用 Unsafe.allocateMemory() 管理缓存和索引数据。
  • Redis 存储引擎:使用 MappedByteBuffer 映射大文件,提高磁盘 I/O。

# 常见错误

  1. 没有限制堆外内存,导致 OOM
    • 解决方案:使用 -XX:MaxDirectMemorySize 限制总量。
  2. 没有主动释放 Unsafe 分配的内存
    • 解决方案:手动 freeMemory(),或使用 Foreign Memory API 代替 Unsafe
  3. 误以为 DirectByteBuffer 由 GC 立即回收
    • 真实情况:DirectByteBuffer 依赖 Cleaner 回收,可能不及时。

# 最佳实践

  • 优先使用 DirectByteBuffer,并限制 MaxDirectMemorySize
  • 避免使用 Unsafe.allocateMemory(),改用 Foreign Memory API(JDK 14+)。
  • 使用 Cleaner 或对象池管理堆外内存,防止 OOM。

# 深入追问

  1. 如何检测堆外内存泄漏?
  2. DirectByteBuffer 为什么比 HeapByteBuffer 速度快?
  3. 如何通过 jcmdNMT 监控堆外内存使用?
  4. Netty 如何优化堆外内存的分配?
  5. 如何避免 MappedByteBuffer 导致的 java.nio.BufferOverflowException

# 相关面试题

  • DirectByteBufferHeapByteBuffer 的区别是什么?
  • Netty 如何管理堆外内存?
  • 为什么 MappedByteBuffer 适合大文件存储?