直接内存与NIO

前面几篇讨论的运行时数据区——堆、栈、方法区——都在JVM的内存管理范畴内。但有一块内存游离于JVM堆之外,却对Java的高性能IO至关重要——直接内存(Direct Memory)。它不是JVM运行时数据区的一部分,却通过NIO的DirectByteBuffer与Java应用紧密协作。本篇将剖析直接内存的概念、NIO ByteBuffer的设计、Unsafe.allocateMemory的底层原理、DirectByteBuffer的Cleaner回收机制、零拷贝技术,以及堆外内存的实践要点。

直接内存的概念

什么是直接内存

直接内存(Direct Memory)是指通过Java代码直接向操作系统申请的堆外内存(Off-Heap Memory),不归JVM垃圾回收器管理,也不受-Xmx堆大小限制(但受-XX:MaxDirectMemorySize约束)。

JVM 进程内存布局
┌──────────────────────────────────────────────┐
│              JVM 进程内存                      │
│                                            │
│  ┌──────────────────┐  ┌────────────────┐  │
│  │  Java堆           │  │  直接内存       │  │
│  │  (-Xmx 控制)      │  │  (堆外内存)     │  │
│  │  ┌──────────────┐ │  │  ┌────────────┐ │  │
│  │  │ 新生代 老年代 │ │  │  │ DirectByteBuffer│ │
│  │  └──────────────┘ │  │  │ 管理的内存  │ │  │
│  └──────────────────┘  │  └────────────┘ │  │
│                        └────────────────┘  │
│  ┌──────────────────┐  ┌────────────────┐  │
│  │  元空间           │  │  线程栈         │  │
│  └──────────────────┘  └────────────────┘  │
└──────────────────────────────────────────────┘

直接内存与JVM堆的关键区别:

对比维度 Java堆 直接内存
分配方式 new关键字,JVM管理 ByteBuffer.allocateDirect()Unsafe
GC管理 由GC自动回收 通过Cleaner机制间接回收
大小限制 -Xmx -XX:MaxDirectMemorySize
数据拷贝 与IO操作需经过堆 可直接与IO通道交互
分配速度 快(TLAB内) 慢(系统调用)

为什么需要直接内存

Java传统的IO(java.io包)是基于的,读写数据需要经过JVM堆中转:

传统IO读文件:
操作系统内核缓冲区 ──拷贝──→ Java堆中的byte[]

  read()系统调用:
  磁盘 → 内核页缓存 → 用户态堆内存
                    ↑
                    一次数据拷贝 (内核→用户态)

这种模式下,数据从内核缓冲区拷贝到Java堆中,多了一次内存拷贝。对于高吞吐的IO场景(如网络中间件、大数据处理),这个开销不可忽视。

直接内存的思路是:在堆外分配一块内存,让操作系统直接将数据读入这块内存,Java代码直接操作这块内存,避免了内核缓冲区到堆的拷贝。

NIO DirectByteBuffer读文件:
操作系统内核缓冲区 ──直接写入──→ 直接内存 (堆外)

  read()系统调用:
  磁盘 → 内核页缓存 → 直接内存 (堆外)
                     ↑
                     Java代码直接操作

NIO ByteBuffer

HeapByteBuffer vs DirectByteBuffer

Java NIO的ByteBuffer有两种实现:

// 适用: JDK 8/11/17
import java.nio.ByteBuffer;

public class ByteBufferDemo {
    public static void main(String[] args) {
        // 1. 堆内缓冲区 (HeapByteBuffer)
        ByteBuffer heapBuf = ByteBuffer.allocate(1024);
        // 底层是 byte[] array, 在Java堆中分配
        // 优点: 分配快, GC管理
        // 缺点: IO操作需额外拷贝

        // 2. 直接缓冲区 (DirectByteBuffer)
        ByteBuffer directBuf = ByteBuffer.allocateDirect(1024);
        // 底层是堆外内存, 通过Unsafe.allocateMemory分配
        // 优点: IO零拷贝
        // 缺点: 分配慢, 回收不确定

        System.out.println(heapBuf.isDirect());   // false
        System.out.println(directBuf.isDirect()); // true

        // 3. 包装已有数组的缓冲区 (非直接)
        byte[] data = new byte[1024];
        ByteBuffer wrappedBuf = ByteBuffer.wrap(data);
        System.out.println(wrappedBuf.isDirect()); // false
    }
}

两种Buffer的IO路径差异

理解性能差异需要看IO操作的完整路径:

HeapByteBuffer 的 write() 路径:
┌──────────┐    拷贝     ┌──────────┐    write()    ┌──────────┐
│ Java堆   │ ──────────→ │ 临时直接 │ ──────────→  │ 内核缓冲 │
│ byte[]   │             │ 内存     │              │ 区       │
└──────────┘             └──────────┘              └──────────┘
                          ↑
                     JVM内部临时分配
                     (每次IO都需拷贝)

DirectByteBuffer 的 write() 路径:
┌──────────┐             write()            ┌──────────┐
│ 直接内存 │ ──────────────────────────→    │ 内核缓冲 │
│ (堆外)   │                                │ 区       │
└──────────┘                                └──────────┘
       ↑
  无需额外拷贝, 直接传内存地址给操作系统

HeapByteBuffer进行IO操作时,JVM为了防止IO过程中GC移动堆内存导致地址失效,会临时分配一块直接内存,将堆中数据拷贝过去,再进行IO。这个临时分配和拷贝在每次IO调用时都发生。

DirectByteBuffer本身就是堆外内存,地址固定,IO操作直接使用其内存地址,无需额外拷贝。

代码示例:性能对比

// 适用: JDK 8/11/17
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.io.*;

public class BufferPerfCompare {
    private static final int BUFFER_SIZE = 64 * 1024;  // 64KB
    private static final long FILE_SIZE = 500 * 1024 * 1024;  // 500MB

    // 使用HeapByteBuffer写文件
    public static long writeWithHeap(File f) throws Exception {
        long start = System.nanoTime();
        try (FileChannel ch = new FileOutputStream(f).getChannel()) {
            ByteBuffer buf = ByteBuffer.allocate(BUFFER_SIZE);
            while (buf.remaining() > 0 || ch.position() < FILE_SIZE) {
                if (!buf.hasRemaining()) buf.clear();
                buf.put((byte) 1);
                if (!buf.hasRemaining() || ch.position() + buf.position() >= FILE_SIZE) {
                    buf.flip();
                    ch.write(buf);
                    buf.clear();
                }
            }
        }
        return System.nanoTime() - start;
    }

    // 使用DirectByteBuffer写文件
    public static long writeWithDirect(File f) throws Exception {
        long start = System.nanoTime();
        try (FileChannel ch = new FileOutputStream(f).getChannel()) {
            ByteBuffer buf = ByteBuffer.allocateDirect(BUFFER_SIZE);
            while (buf.remaining() > 0 || ch.position() < FILE_SIZE) {
                if (!buf.hasRemaining()) buf.clear();
                buf.put((byte) 1);
                if (!buf.hasRemaining() || ch.position() + buf.position() >= FILE_SIZE) {
                    buf.flip();
                    ch.write(buf);
                    buf.clear();
                }
            }
        }
        return System.nanoTime() - start;
    }

    public static void main(String[] args) throws Exception {
        File f1 = new File("test_heap.dat");
        File f2 = new File("test_direct.dat");

        // 预热
        System.out.println("HeapByteBuffer:   " + writeWithHeap(f1) / 1_000_000 + " ms");
        System.out.println("DirectByteBuffer: " + writeWithDirect(f2) / 1_000_000 + " ms");

        f1.delete(); f2.delete();
    }
}

典型输出(500MB文件,64KB缓冲区):

HeapByteBuffer:   约 1200-1500 ms
DirectByteBuffer: 约 800-1000 ms

差距会随IO量增大而明显。但注意:对于小数据量、单次IO,DirectByteBuffer的分配开销可能抵消零拷贝的收益

选择策略

场景 推荐Buffer 原因
大文件读写、高吞吐网络IO DirectByteBuffer 零拷贝收益大
小数据量、频繁分配 HeapByteBuffer 分配快,GC即回收
需要频繁随机访问数组内容 HeapByteBuffer 堆内访问快,有数组边界优化
长期复用的IO缓冲区 DirectByteBuffer 复用避免重复分配开销
一次性的短生命周期缓冲区 HeapByteBuffer 避免直接内存分配+回收的额外成本

Unsafe.allocateMemory

底层分配机制

DirectByteBuffer的底层内存分配依赖sun.misc.Unsafe类:

// HotSpot JDK 8 中 DirectByteBuffer 的核心源码 (简化)
class DirectByteBuffer extends MappedByteBuffer {
    // Unsafe实例
    private static final Unsafe unsafe = Unsafe.getUnsafe();

    // 堆外内存地址
    private long address;

    // Cleaner用于回收
    private final Cleaner cleaner;

    DirectByteBuffer(int cap) {
        super(-1, 0, cap, cap);
        // 通过BitMap分配内存, 记录全局直接内存使用量
        boolean pa = VM.isDirectBufferMemorySettingAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? pageSize : 0));

        // 关键: 调用Unsafe分配堆外内存
        base = unsafe.allocateMemory(size);
        // 内存清零
        unsafe.setMemory(base, size, (byte) 0);

        // 记录内存地址
        address = unsafe.realAddress(base);

        // 注册Cleaner, GC时通过Deallocator释放内存
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    }
}

Unsafe.allocateMemory的native实现最终调用操作系统的malloc(或mmap):

Java层: ByteBuffer.allocateDirect(1024)
    ↓
DirectByteBuffer构造函数
    ↓
Bits.reserveMemory()  // 检查直接内存配额
    ↓
Unsafe.allocateMemory(size)  // native方法
    ↓
os::malloc(size)  // HotSpot C++层
    ↓
malloc() / mmap()  // 操作系统系统调用

直接使用Unsafe分配内存

可以直接使用Unsafe操作堆外内存(生产不推荐,仅用于理解原理):

// 适用: JDK 8 (JDK 9+ 需要模块化反射访问)
import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class UnsafeMemoryDemo {
    public static void main(String[] args) throws Exception {
        // 通过反射获取Unsafe实例 (Unsafe.getUnsafe()会检查调用者)
        Field f = Unsafe.class.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);

        // 分配100字节堆外内存
        long address = unsafe.allocateMemory(100);
        System.out.println("分配地址: 0x" + Long.toHexString(address));

        // 写入数据
        unsafe.putLong(address, 123456789L);        // 写8字节long
        unsafe.putByte(address + 8, (byte) 42);     // 写1字节byte
        unsafe.putInt(address + 9, 1000);           // 写4字节int

        // 读取数据
        long val1 = unsafe.getLong(address);
        byte val2 = unsafe.getByte(address + 8);
        int val3 = unsafe.getInt(address + 9);
        System.out.println(val1 + ", " + val2 + ", " + val3);
        // 输出: 123456789, 42, 1000

        // 释放内存 (必须手动释放, 否则内存泄漏!)
        unsafe.freeMemory(address);
    }
}

关键点:

  • allocateMemory返回的是内存地址(long类型的指针),不是Java对象
  • 堆外内存不受GC管理,必须手动freeMemory释放
  • 没有 bounds checking,越界访问直接crash JVM
  • JDK 9+对sun.misc.Unsafe做了模块化封装,推荐使用VarHandleAPI

直接内存配额检查

Bits.reserveMemory()在分配前检查直接内存配额:

分配流程:
1. Bits.reserveMemory(size)
   ├── 检查 总已用 + size <= MaxDirectMemorySize?
   ├── 如果超限:
   │   ├── 触发System.gc() (希望GC回收无用的DirectByteBuffer)
   │   ├── 等待一段时间
   │   └── 如果还不够 → OutOfMemoryError: Direct buffer memory
   └── 如果OK: 增加全局计数器

2. unsafe.allocateMemory(size) → 实际分配

这就是为什么设置了-XX:MaxDirectMemorySize后,直接内存OOM前通常会先看到一次Full GC——JVM在尝试回收闲置的DirectByteBuffer。

-XX:MaxDirectMemorySize

参数说明

# 限制直接内存最大为512MB
java -XX:MaxDirectMemorySize=512m -jar app.jar
参数 默认值 说明
-XX:MaxDirectMemorySize 约等于-Xmx 直接内存上限

默认值与-Xmx一致(不严格相等,略有偏差)。这个默认值的设计初衷是:大多数应用不会大量使用直接内存,设为与堆相同比较宽松。但在使用Netty等IO框架时,直接内存使用量可能很大,需要单独设置。

OOM复现

// 适用: JDK 8/11/17
// 运行: java -XX:MaxDirectMemorySize=64m DirectOOM
import java.nio.ByteBuffer;

public class DirectOOM {
    public static void main(String[] args) {
        java.util.List<ByteBuffer> list = new java.util.ArrayList<>();
        try {
            while (true) {
                // 每次分配1MB直接内存, 保持引用防止回收
                ByteBuffer buf = ByteBuffer.allocateDirect(1024 * 1024);
                list.add(buf);
                System.out.println("已分配: " + list.size() + " MB");
            }
        } catch (OutOfMemoryError e) {
            e.printStackTrace();
            // java.lang.OutOfMemoryError: Direct buffer memory
        }
    }
}

输出:

已分配: 1 MB
已分配: 2 MB
...
已分配: 62 MB
java.lang.OutOfMemoryError: Direct buffer memory

直接内存的监控

# 查看直接内存使用 (JMX方式, 通过jcmd)
jcmd <pid> GC.class_histogram

# 通过jstat无法直接查看直接内存, 需用JMX
# 使用jconsole或JVisualVM的MBean查看:
#   java.nio:type=BufferPool,name=direct
#   → count, memoryUsed, totalCapacity

# Arthas查看
[arthas@1234]$ memory   # 显示各内存区域, 包括direct

# Native内存追踪 (JDK 11+)
jcmd <pid> VM.native_memory summary
# 查看Internal部分 (包含DirectMemory)

JDK 11+的NMT(Native Memory Tracking)可以追踪本地内存使用:

# 启动时开启NMT
java -XX:NativeMemoryTracking=summary -jar app.jar

# 查看直接内存
jcmd <pid> VM.native_memory summary | grep -A5 "Internal"

DirectByteBuffer的Cleaner机制

PhantomReference + Cleaner

DirectByteBuffer本身是一个Java对象(在堆中),它引用了一块堆外内存。当DirectByteBuffer对象被GC回收时,堆外内存也需要被释放——这就是Cleaner机制的作用。

Java堆                              堆外内存
┌──────────────────┐               ┌──────────────┐
│ DirectByteBuffer │ ──address───→ │ 1MB内存      │
│ (Java对象)        │               │ (malloc分配) │
└──────────────────┘               └──────────────┘
        ↑
  GC可达时: 对象存活, 堆外内存保留
  GC不可达时: Cleaner触发回收

Cleaner机制的核心是虚引用(PhantomReference):

// HotSpot JDK 8 中 Cleaner 的核心源码 (简化)
public class Cleaner extends PhantomReference<Object> {
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();
    private Cleaner next, prev;
    private final Runnable thunk;  // 回收动作

    // 创建Cleaner: obj被GC回收时, 执行thunk
    public static Cleaner create(Object obj, Runnable thunk) {
        return add(new Cleaner(obj, thunk));
    }

    // ReferenceHandler线程轮询, 发现Cleaner被回收时执行
    @Override
    public void clean() {
        if (remove(this)) {
            try {
                thunk.run();  // 执行Deallocator.run()
            } catch (Throwable x) {
                ...
            }
        }
    }
}

DirectByteBuffer的Deallocator:

// Deallocator: 实际释放堆外内存的Runnable
private static class Deallocator implements Runnable {
    private long address;
    private long size;
    private int capacity;

    public void run() {
        if (address == 0) return;
        // 调用Unsafe释放堆外内存
        unsafe.freeMemory(address);
        // 减少全局直接内存计数
        Bits.unreservedMemory(size, capacity);
        address = 0;
    }
}

回收的完整流程

1. DirectByteBuffer对象变为GC不可达
   (应用不再引用该buffer)

2. GC扫描发现DirectByteBuffer可回收
   ├── 普通对象: 直接回收堆内内存
   └── 有Cleaner的对象: 将Cleaner加入ReferenceQueue

3. ReferenceHandler后台线程 (优先级最高)
   ├── 轮询ReferenceQueue
   ├── 发现Cleaner
   └── 调用Cleaner.clean() → Deallocator.run()
       └── unsafe.freeMemory(address)  // 释放堆外内存

4. 堆外内存被释放, 返还给操作系统

关键点:

  1. 堆内对象和堆外内存的回收是异步的——DirectByteBuffer对象在GC时回收,但堆外内存要等ReferenceHandler线程执行freeMemory才释放。

  2. ReferenceHandler是JVM的内部线程java.lang.ref.Reference$ReferenceHandler),优先级最高(MAX_PRIORITY),确保及时处理引用。

  3. 如果DirectByteBuffer一直被引用,堆外内存不会被释放。常见场景:Netty的ByteBuf未release导致内存泄漏。

  4. System.gc()的配合:直接内存分配失败时,JVM会调用System.gc()触发Full GC,期望回收不可达的DirectByteBuffer。如果应用禁用了System.gc()-XX:+DisableExplicitGC),可能导致直接内存OOM提前出现。

Cleaner vs finalize

Cleaner机制与finalize()方法有相似之处(都是对象回收时的回调),但有关键区别:

对比 finalize() Cleaner/PhantomReference
引用阶段 FinalReference (强/弱) PhantomReference (虚)
调用时机 GC前加入Finalizer队列 GC后进入ReferenceQueue
对象状态 对象可能被"复活" 对象已确定被回收
线程 Finalizer线程 ReferenceHandler线程
JDK状态 JDK 9+废弃 JDK 9+推荐替代finalize

虚引用的优势:对象进入Cleaner时已经被GC判定为不可达,不会被"复活",回收更安全。而finalize()中可以重新建立引用,导致对象复活,引发难以排查的问题。

零拷贝

零拷贝的含义

“零拷贝”(Zero Copy)不是真的零次拷贝,而是指减少CPU参与的数据拷贝次数,将拷贝交给DMA(Direct Memory Access)硬件完成。

传统read+write (4次拷贝, 4次上下文切换):
用户态                内核态
┌─────┐  read()   ┌────────┐
│ app │ ────────→ │ 读取磁盘│
│     │           │ 内核缓冲│
│     │ ←──────── │   ↓    │
│     │  数据拷贝  │ 拷贝到  │
│ buf │           │ 用户态  │
│     │  write()  │   ↓    │
│     │ ────────→ │ 拷贝回  │
│     │           │ 内核缓冲│
│     │           │   ↓    │
│     │           │ DMA→网卡│
└─────┘           └────────┘

零拷贝transferTo (2次拷贝, 2次上下文切换):
用户态                内核态
┌─────┐ transferTo ┌────────┐
│ app │ ────────→ │ DMA读磁盘│
│     │           │   ↓     │
│     │           │ 内核缓冲 │
│     │           │   ↓     │
│     │           │ DMA→网卡 │
└─────┘           └────────┘
   ↑
 数据从不进入用户态

transferTo / transferFrom

FileChannel.transferTo()利用操作系统的sendfile系统调用,实现文件到网络的零拷贝:

// 适用: JDK 8/11/17
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
import java.io.*;

public class ZeroCopyDemo {
    // 零拷贝: 文件 → 网络
    public static void sendFile(FileChannel fileCh, 
            SocketChannel socketCh) throws IOException {
        long transferred = 0;
        long fileSize = fileCh.size();
        // transferTo: 内部调用 sendfile (Linux) / TransmitFile (Windows)
        while (transferred < fileSize) {
            transferred += fileCh.transferTo(
                transferred, fileSize - transferred, socketCh);
        }
    }

    // 零拷贝: 网络 → 文件
    public static void recvFile(SocketChannel socketCh,
            FileChannel fileCh) throws IOException {
        fileCh.transferFrom(socketCh, 0, Long.MAX_VALUE);
    }
}

常见零拷贝技术对比

技术 系统调用 拷贝次数 适用场景
传统read+write read+write 4次(2次CPU+2次DMA) 通用
mmap+write mmap+write 3次(1次CPU+2次DMA) 需修改数据
sendfile sendfile 3次(1次CPU+2次DMA) 文件→socket
sendfile+SG-DMA sendfile (scatter-gather) 2次(0次CPU+2次DMA) 文件→socket,硬件支持

transferTo在Linux上优先使用sendfile+scatter-gather DMA,实现真正的零CPU拷贝。

零拷贝的性能优势

// 适用: JDK 8/11/17
// 对比传统IO传输 vs 零拷贝传输
import java.io.*;
import java.nio.channels.*;

public class ZeroCopyPerf {
    private static final long FILE_SIZE = 1024 * 1024 * 500; // 500MB

    // 传统IO: read到byte[]再write
    public static long traditionalTransfer(
            FileChannel src, WritableByteChannel dest) throws IOException {
        long start = System.nanoTime();
        ByteBuffer buf = ByteBuffer.allocate(64 * 1024);
        while (src.read(buf) != -1) {
            buf.flip();
            dest.write(buf);
            buf.clear();
        }
        return System.nanoTime() - start;
    }

    // 零拷贝: transferTo
    public static long zeroCopyTransfer(
            FileChannel src, WritableByteChannel dest) throws IOException {
        long start = System.nanoTime();
        src.transferTo(0, src.size(), dest);
        return System.nanoTime() - start;
    }
}

典型结果(500MB文件,本地传输):

传统IO:   约 1500-2000 ms
零拷贝:   约 400-600 ms

零拷贝优势随文件增大而放大。Kafka、Netty等高性能框架大量使用transferToFileChannel.map(mmap)实现零拷贝。

实践要点

  1. 合理设置-XX:MaxDirectMemorySize:使用Netty、NIO框架的应用,直接内存可能占用较大。建议显式设置(如堆大小的1/2到1倍),并通过JMX监控实际使用量。不设置时默认与-Xmx一致,可能导致OOM Killer先于Java OOM触发。

  2. 慎用-XX:+DisableExplicitGC:这个参数禁用System.gc(),但JVM在直接内存分配失败时会依赖System.gc()触发Full GC来回收闲置的DirectByteBuffer。禁用后可能导致直接内存OOM提前出现。如果必须禁用,建议配合-XX:MaxDirectMemorySize留足余量。

  3. DirectByteBuffer的复用:直接内存分配开销大(系统调用+内存清零),适合长期复用。Netty使用PooledDirectByteBufAllocator实现堆外内存池化,避免频繁分配释放。如果自行管理DirectByteBuffer,建议用对象池复用。

  4. 内存泄漏排查:DirectByteBuffer泄漏比堆内存泄漏更难排查——堆外内存不在heap dump中。排查方法:

    • 使用JMX监控java.nio:type=BufferPool,name=directmemoryUsed
    • JDK 11+用jcmd VM.native_memory summary查看Internal区域
    • Netty提供了ResourceLeakDetector检测ByteBuf泄漏
    • 使用-Dio.netty.leakDetection.level=PARANOID(Netty)
  5. 堆外内存的生命周期管理:DirectByteBuffer的回收依赖GC,如果应用-XX:MaxDirectMemorySize设得大、堆也大、GC不频繁,直接内存可能堆积。建议在高吞吐IO场景下主动管理DirectByteBuffer的生命周期(如Netty的引用计数)。

  6. DirectByteBuffer vs Unsafe的选择:业务代码应优先使用ByteBuffer.allocateDirect,它有完整的配额管理和Cleaner回收机制。直接使用Unsafe.allocateMemory需要自行管理内存生命周期,容易泄漏,仅适合框架级开发。

  7. JDK版本差异:JDK 9+对sun.misc.Unsafe做了模块化封装,allocateMemory等方法在JDK 9-16通过反射仍可访问,JDK 17+严格限制。新代码应考虑使用MemorySegment(JDK 14+ incubator,JDK 16+正式),它是更安全的堆外内存API。

  8. 不要忽略ReferenceHandler线程:如果应用有大量DirectByteBuffer且频繁GC,ReferenceHandler线程可能成为瓶颈。监控其CPU占用,必要时减少DirectByteBuffer的创建频率。

小结

  • 直接内存是堆外内存,不归GC管理,通过-XX:MaxDirectMemorySize限制大小。它让IO操作无需经过Java堆中转,是高性能IO的基础。
  • NIO ByteBuffer分HeapByteBuffer(堆内,分配快但IO有额外拷贝)和DirectByteBuffer(堆外,IO零拷贝但分配慢)。大文件和高吞吐网络IO用Direct,小数据用Heap。
  • Unsafe.allocateMemory是直接内存分配的底层,最终调用malloc。直接使用Unsafe需手动freeMemory,容易泄漏,应优先用ByteBuffer.allocateDirect
  • Cleaner机制通过PhantomReference在DirectByteBuffer被GC回收时释放对应的堆外内存,ReferenceHandler线程负责执行释放。回收是异步的,依赖GC触发。
  • 零拷贝通过transferTo/sendfile减少CPU参与的拷贝,将数据传输交给DMA,是Kafka、Netty等高性能框架的核心优化手段。

更多内容:JVM调优实战

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐