Skip to content

直接内存(使用/测试/OOM)

直接内存:NIO 的幕后功臣

直接内存(Direct Memory) 不是 JVM 运行时数据区的一部分,但它与 Java NIO 密切相关,对 Java 程序的性能有重要影响。

什么是直接内存

直接内存是操作系统分配的本地内存(Native Memory),通过 java.nio.DirectByteBuffer 与 Java 堆中的对象交互。

┌──────────────────────────────────────────────┐
│                   Java 堆                         │
│   DirectByteBuffer 对象(持有直接内存地址)        │
└──────────────────────┬───────────────────────────┘
                       │  引用

┌──────────────────────────────────────────────┐
│                 直接内存(本地内存)               │
│   真正存储数据的缓冲区(操作系统分配)            │
└──────────────────────────────────────────────┘

为什么需要直接内存

传统 IO 的问题:多次拷贝

传统 IO(FileInputStream / BufferedInputStream)的数据流转:

应用程序缓冲区

      │ 拷贝 1(内核缓冲区 → 用户缓冲区)

内核缓冲区(Page Cache)

      │ 拷贝 2(磁盘 → 内核缓冲区)

磁盘

数据需要经过两次拷贝:磁盘 → 内核缓冲区 → 用户缓冲区。

直接内存的优势:零拷贝

NIO 的直接内存可以做到零拷贝

直接内存缓冲区(用户空间)

      │ 零拷贝(通过 DMA)

网卡 / 磁盘

或者:

直接内存缓冲区

      │ 映射

内核缓冲区

      │ 映射

应用缓冲区

直接内存可以避免数据在用户缓冲区和内核缓冲区之间的拷贝,大幅提升 IO 性能。

直接内存的使用场景

场景一:高性能网络框架(Netty)

Netty 是最典型的直接内存使用者:

java
// Netty 使用直接内存进行高性能网络传输
// 底层 ByteBuf 默认为 PooledDirectByteBuf(池化的直接内存)
public class NettyDirectMemory {
    public static void main(String[] args) {
        // Netty NIO 默认使用直接内存
        // 每个 ChannelHandlerContext 都会分配直接内存
        // 如果直接内存设置不当,会抛出 OOM
    }
}

场景二:NIO 文件操作

java
public class NIOFileDirect {
    public static void main(String[] args) throws Exception {
        // 使用直接内存进行文件读写
        FileChannel channel = new RandomAccessFile("data.txt", "rw")
            .getChannel();

        // 分配直接内存缓冲区
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);  // 1MB 直接内存

        channel.read(buffer);
        buffer.flip();
        channel.write(buffer);
    }
}

场景三:高性能消息队列(Kafka)

Kafka 在消息存储和网络传输中大量使用直接内存,避免数据在内核缓冲区和用户缓冲区之间反复拷贝。

直接内存的配置

bash
# JDK 8 及之前:默认直接内存大小 = 堆最大大小(-Xmx)
# 最大不超过 64GB(受寻址空间限制)
java -XX:MaxDirectMemorySize=2g MyApp

# JDK 9+:默认直接内存大小 = 堆最大大小
# 推荐明确设置
java -XX:MaxDirectMemorySize=1g MyApp

直接内存大小的计算

直接内存 ≈ NIO 使用的 ByteBuffer 总大小
         = Channel 缓冲区 + 其他 NIO 组件

Netty 的直接内存计算示例:

bash
# Netty 直接内存 = Arena 数量 × Arena 大小 × 页面大小
# Arena 数量 = 2 × CPU 核心数(JVM 默认)
# Arena 大小 = chunkSize × pageCache
#
# 如果 Netty 设置:io.netty.allocator.numDirectArenas=8
# 每个 Arena 默认 64MB
# 那么 Netty 直接内存 ≈ 8 × 64MB = 512MB

直接内存 OOM

直接内存耗尽时,会抛出 OutOfMemoryError: Direct buffer memory

java
public class DirectMemoryOOM {
    public static void main(String[] args) throws Exception {
        // 不断分配直接内存,直到耗尽
        List<ByteBuffer> buffers = new ArrayList<>();
        try {
            while (true) {
                // 每次分配 256MB 直接内存
                ByteBuffer buffer = ByteBuffer.allocateDirect(256 * 1024 * 1024);
                buffers.add(buffer);
            }
        } catch (OutOfMemoryError e) {
            System.out.println("OOM after " + buffers.size() + " buffers");
            throw e;
        }
    }
}
java.lang.OutOfMemoryError: Direct buffer memory

直接内存的回收

DirectByteBuffer 对象在堆中,但它们引用的直接内存在本地内存中。当 DirectByteBuffer 被 GC 回收时,JVM 会通过 Cleaner 异步释放直接内存:

java
public class DirectMemoryCleanup {
    public static void main(String[] args) throws Exception {
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
        // buffer 对象在堆中,受 GC 管理
        // 当 buffer 不再被引用,GC 会触发 Cleaner
        // Cleaner 会在后台线程中释放直接内存

        buffer = null;
        System.gc();  // 建议触发 GC,释放直接内存
    }
}

直接内存与堆内存的对比

维度堆内存直接内存
位置JVM 堆操作系统本地内存
分配new byte[]ByteBuffer.allocateDirect()
分配速度慢(需要 native 调用)
访问速度较快快(避免一次拷贝)
GC受堆 GC 管理Cleaner 异步回收
OOMJava heap spaceDirect buffer memory
适用场景普通对象高性能 IO、网络传输

直接内存监控

bash
# 查看直接内存使用(JDK 8+)
jstat -gc <pid>
# 输出中有:OC=老年代容量, UC=老年代已用
# 直接内存不直接显示在 GC 统计中

# 使用 ManagementFactory 监控
java -XX:MaxDirectMemorySize=1g \
     -Dcom.sun.management.jmxremote \
     MyApp
# 通过 JMX Console 可以看到 DirectMemory 的使用

# JDK 11+ 可以用 NativeMemoryTracking
java -XX:NativeMemoryTracking=summary MyApp
jcmd <pid> VM.native_memory baseline
# 运行一段时间后
jcmd <pid> VM.native_memory summary.diff

本节小结

直接内存的核心要点:

关键点说明
是什么操作系统本地内存,不在 JVM 堆内
用途高性能 IO(Netty、Kafka、NIO)
配置-XX:MaxDirectMemorySize
回收Cleaner 异步释放,依赖 GC
OOMOutOfMemoryError: Direct buffer memory
优势零拷贝,适合高 IO 场景

直接内存虽然不在 JVM 堆内,但它受 MaxDirectMemorySize 限制,合理配置对高性能应用非常重要。

到这里,「JVM 运行时数据区」部分的最后一个文件也完成了。接下来进入 运行时数据区总结与大厂面试题,以及 对象实例化专题

基于 VitePress 构建