直接内存(使用/测试/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 异步回收 |
| OOM | Java heap space | Direct 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 |
| OOM | OutOfMemoryError: Direct buffer memory |
| 优势 | 零拷贝,适合高 IO 场景 |
直接内存虽然不在 JVM 堆内,但它受 MaxDirectMemorySize 限制,合理配置对高性能应用非常重要。
到这里,「JVM 运行时数据区」部分的最后一个文件也完成了。接下来进入 运行时数据区总结与大厂面试题,以及 对象实例化专题。
