JVM说--直接内存的使用
字数 1539 2025-08-11 17:40:22

JVM直接内存使用详解

1. 前言

学习JVM底层原理不仅是为了实际应用,更重要的是理解其设计思想和思路。当遇到棘手问题时,这些知识能提供更多解决方案。本文将深入探讨JVM直接内存的使用,特别是NIO中直接内存的应用原理。

2. NIO与IO读写文件效率对比

2.1 测试代码分析

public class DirectBufferTest {
    private static final int SIZE_10MB = 10 * 1024 * 1024;
    
    public static void main(String[] args) throws InterruptedException {
        // 测试三种方式:IO、NIO直接内存、NIO堆内存
        Thread commonIo = new Thread(commonIo(filePath1, fileByteLength, toPath1));
        Thread nioWithDirectBuffer = new Thread(nioWithDirectBuffer(filePath2, fileByteLength, toPath2));
        Thread nioWithHeapBuffer = new Thread(nioWithHeapBuffer(filePath3, fileByteLength, toPath3));
        
        nioWithDirectBuffer.start();
        commonIo.start();
        nioWithHeapBuffer.start();
    }
    
    // IO方式
    public static void commonIo(String filePath, Integer byteLength, String toPath) {
        try (FileInputStream fis = new FileInputStream(filePath);
             FileOutputStream fos = new FileOutputStream(toPath)) {
            byte[] readByte = new byte[byteLength];
            int readCount;
            while ((readCount = fis.read(readByte)) != -1) {
                fos.write(readByte, 0, readCount);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    // NIO直接内存方式
    public static void nioWithDirectBuffer(String filePath, Integer byteLength, String toPath) {
        try (FileChannel fci = new RandomAccessFile(filePath, "rw").getChannel();
             FileChannel fco = new RandomAccessFile(toPath, "rw").getChannel()) {
            ByteBuffer bb = ByteBuffer.allocateDirect(byteLength);
            while (true) {
                int len = fci.read(bb);
                if (len == -1) break;
                bb.flip();
                fco.write(bb);
                bb.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    // NIO堆内存方式
    public static void nioWithHeapBuffer(String filePath, Integer byteLength, String toPath) {
        try (FileChannel fci = new RandomAccessFile(filePath, "rw").getChannel();
             FileChannel fco = new RandomAccessFile(toPath, "rw").getChannel()) {
            ByteBuffer bb = ByteBuffer.allocate(byteLength);
            while (true) {
                int len = fci.read(bb);
                if (len == -1) break;
                bb.flip();
                fco.write(bb);
                bb.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.2 测试结果

多次测试后,性能对比结果如下:

  1. NIO直接内存方式:1157ms
  2. IO方式:1704ms
  3. NIO堆内存方式:3038ms

结论:NIO读取文件效率高于IO,尤其是读取大文件时,而NIO性能优势主要来自于直接内存的使用。

3. 直接内存读写性能原理

3.1 堆内存读写文件流程

  1. 操作系统读取磁盘数据到系统内存缓冲区
  2. JVM将数据从系统内存缓冲区拷贝到JVM堆内存
  3. 应用程序从JVM堆内存获取数据

3.2 直接内存读写文件流程

  1. 调用native方法allocateMemory分配直接内存
  2. 操作系统直接将文件读取到直接内存
  3. 应用程序通过JVM堆空间的DirectByteBuffer读取数据

关键区别:直接内存方式减少了数据拷贝过程,避免了不必要的性能开销。

4. NIO使用直接内存的源码解析

4.1 关键概念

4.1.1 虚引用Cleaner

  • sun.misc.Cleaner继承自PhantomReference
  • 必须关联一个引用队列ReferenceQueue
  • 作用:JVM将其对应的Cleaner加入pending-Reference链表,通知ReferenceHandler线程处理,最终调用Cleaner#clean方法

4.1.2 Unsafe类

  • sun.misc.Unsafe提供低级别、不安全操作的方法
  • 功能:直接访问系统内存资源、自主管理内存资源等

4.2 直接内存申请过程

// ByteBuffer.allocateDirect方法
public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

// DirectByteBuffer构造函数
DirectByteBuffer(int cap) {
    // 1. 调用Unsafe的native方法allocateMemory申请内存
    long base = unsafe.allocateMemory(size);
    // 2. 设置内存空间
    unsafe.setMemory(base, size, (byte) 0);
    // 3. 创建Cleaner虚引用
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
}

4.3 直接内存释放过程

  1. 新建虚引用:调用Cleaner.create(),将Cleaner加入链表
  2. 声明清理任务Deallocator实现Runnable接口,run()方法调用unsafe.freeMemory()
  3. ReferenceHandler调用
    • ReferenceHandler线程优先级最高
    • 调用tryHandlePending方法
    • 最终调用Cleaner.clean()方法
    • clean()调用Deallocator.run()释放内存

5. 直接内存特性与使用

5.1 直接内存特性

  • NIO常用作数据缓冲区(ByteBuffer)
  • 不受JVM垃圾回收管理,分配和回收成本较高
  • 读写性能非常高

5.2 直接内存相关问题

5.2.1 内存溢出

  • 直接内存使用系统内存
  • 可通过JVM参数控制:-XX:MaxDirectMemorySize=5M

5.2.2 GC影响

  • 调用System.gc()会触发FullGC,可能释放直接内存
  • 但不建议显式调用System.gc(),原因:
    1. 导致stop the world,影响性能
    2. 可能被JVM参数-XX:+DisableExplicitGC禁止

5.3 手动操作直接内存示例

public class UnsafeOperateDirectMemory {
    private static final int SIZE_100MB = 100 * 1024 * 1024;
    
    public static void main(String[] args) {
        Unsafe unsafe = getUnsafePersonal();
        long base = unsafe.allocateMemory(SIZE_100MB);
        unsafe.setMemory(base, SIZE_100MB, (byte) 0);
        unsafe.freeMemory(base);
    }
    
    // 通过反射获取Unsafe实例
    public static Unsafe getUnsafePersonal() {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            return (Unsafe) f.get(null);
        } catch (Exception e) {
            throw new RuntimeException("initial the unsafe failure...");
        }
    }
}

注意:除非对Unsafe有深入理解,否则不建议直接操作。

6. 总结

JVM直接内存是提升I/O性能的重要机制,理解其原理和使用方式对于中高级Java开发者至关重要。关键点包括:

  1. 直接内存减少了数据拷贝,提升I/O性能
  2. NIO通过DirectByteBuffer使用直接内存
  3. 直接内存通过Cleaner机制管理释放
  4. 可配置最大直接内存大小防止OOM
  5. 谨慎操作直接内存,避免性能问题

掌握这些知识有助于在实际开发中做出更合理的技术选型和性能优化。

JVM直接内存使用详解 1. 前言 学习JVM底层原理不仅是为了实际应用,更重要的是理解其设计思想和思路。当遇到棘手问题时,这些知识能提供更多解决方案。本文将深入探讨JVM直接内存的使用,特别是NIO中直接内存的应用原理。 2. NIO与IO读写文件效率对比 2.1 测试代码分析 2.2 测试结果 多次测试后,性能对比结果如下: NIO直接内存方式:1157ms IO方式:1704ms NIO堆内存方式:3038ms 结论 :NIO读取文件效率高于IO,尤其是读取大文件时,而NIO性能优势主要来自于直接内存的使用。 3. 直接内存读写性能原理 3.1 堆内存读写文件流程 操作系统读取磁盘数据到系统内存缓冲区 JVM将数据从系统内存缓冲区拷贝到JVM堆内存 应用程序从JVM堆内存获取数据 3.2 直接内存读写文件流程 调用native方法 allocateMemory 分配直接内存 操作系统直接将文件读取到直接内存 应用程序通过JVM堆空间的 DirectByteBuffer 读取数据 关键区别 :直接内存方式减少了数据拷贝过程,避免了不必要的性能开销。 4. NIO使用直接内存的源码解析 4.1 关键概念 4.1.1 虚引用Cleaner sun.misc.Cleaner 继承自 PhantomReference 必须关联一个引用队列 ReferenceQueue 作用:JVM将其对应的Cleaner加入 pending-Reference 链表,通知 ReferenceHandler 线程处理,最终调用 Cleaner#clean 方法 4.1.2 Unsafe类 sun.misc.Unsafe 提供低级别、不安全操作的方法 功能:直接访问系统内存资源、自主管理内存资源等 4.2 直接内存申请过程 4.3 直接内存释放过程 新建虚引用 :调用 Cleaner.create() ,将Cleaner加入链表 声明清理任务 : Deallocator 实现 Runnable 接口, run() 方法调用 unsafe.freeMemory() ReferenceHandler调用 : ReferenceHandler 线程优先级最高 调用 tryHandlePending 方法 最终调用 Cleaner.clean() 方法 clean() 调用 Deallocator.run() 释放内存 5. 直接内存特性与使用 5.1 直接内存特性 NIO常用作数据缓冲区( ByteBuffer ) 不受JVM垃圾回收管理,分配和回收成本较高 读写性能非常高 5.2 直接内存相关问题 5.2.1 内存溢出 直接内存使用系统内存 可通过JVM参数控制: -XX:MaxDirectMemorySize=5M 5.2.2 GC影响 调用 System.gc() 会触发FullGC,可能释放直接内存 但不建议显式调用 System.gc() ,原因: 导致 stop the world ,影响性能 可能被JVM参数 -XX:+DisableExplicitGC 禁止 5.3 手动操作直接内存示例 注意 :除非对Unsafe有深入理解,否则不建议直接操作。 6. 总结 JVM直接内存是提升I/O性能的重要机制,理解其原理和使用方式对于中高级Java开发者至关重要。关键点包括: 直接内存减少了数据拷贝,提升I/O性能 NIO通过 DirectByteBuffer 使用直接内存 直接内存通过 Cleaner 机制管理释放 可配置最大直接内存大小防止OOM 谨慎操作直接内存,避免性能问题 掌握这些知识有助于在实际开发中做出更合理的技术选型和性能优化。