JVM直接内存详解

直接内存

​ 学习JVM内存结构部分时遇到的最后一部分,直接内存 。虽然和其他堆栈等不是核心部分,但其类似缓存的特点和与GC相关的特性显得有点特殊,比较好奇这个高速缓存有没有实际开发使用场景,所以写这篇博客记录直接内存的相关知识点使用场景

概念

直接内存(Direct Memory)是操作系统内存和Java内存共用的一片内存区域

  • 读写性能高,常见于NIO操作作为数据缓存区
  • 可以通过ByteBuffer.allocateDirect()分配内存区域

由来:

image-20250215174251055

因为 java 不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 java 堆内存中。缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制

image-20250215174402438

直接内存是操作系统和 Java 代码都可以访问的一块区域,无需将代码从系统内存复制到 Java 堆内存,从而提高了效率。

特点

  1. 分配回收成本高,读写性能高

  2. 不受到JVM内存回收管理,而是底层被unsafe对象回收

直接内存的底层分配其实是创建了一个DirectByteBuffer对象,其内部创建了一个Cleaner虚引用来指向DirectByteBuffer

//分配直接内存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
//源码
public static ByteBuffer allocateDirect(int capacity) {
    //创建DirectByteBuffer
    return new DirectByteBuffer(capacity);
}
DirectByteBuffer(int cap) {                 
    ...
    //创建虚引用
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    ...
 }

直接内存的回收涉及到GC中虚引用引用队列的设计:

  • 如果bytebuffer被回收,但是虚拟内存空间不受javagc管理,所以无法回收造成内存泄漏

  • 引入了Cleaner虚引用(记录内存地址)和引用队列,虚引用指向bytebuffer,在bytebuffer被回收后虚引用Cleaner 会加入引用队列,此时会有一个 ReferenceHandler(守护线程) 检测引用队列存在Cleaner后,调用其clean()通过通过调用 unsafe.freeMemory()释放内存

public void clean() {
        if (!remove(this))
            return;
        try {
            thunk.run();//回调DirectByteBuffer中Deallocator的run
        } catch (final Throwable x) {
           ...
        }
    }

public void run() {
           if (address == 0) {
             // Paranoia
             return;
        }
       UNSAFE.freeMemory(address);//通过 unsafe.freeMemory()来释放内存
       address = 0;
       Bits.unreserveMemory(size, capacity);
}
  1. 这个虚拟机参数常用于JVM调优(因为full GC非常耗时,所以会被关闭),但也因此使得直接内存回收失效,此时可以反射获取unsafe调用回收方法
-XX:+DisableExplicitGC  // 禁止显式 GC

这里做一下解释:

由上可知直接内存释放流程

DirectByteBuffer被回收-->关联的Cleaner对象进入引用队列-->守护线程检测到引用队列存在Cleaner,调用其clean()通过 unsafe.freeMemory()来释放内存

因此直接内存的释放依赖 DirectByteBuffer 对象的回收。若该对象未被GC回收,即使它已不可达,关联的直接内存也不会释放。

此处的直接内存回收是指通过将byteBuffer置null,然后调用System.gc()回收DirectByteBuffer,进而触发Cleaner内存回收机制

  private static void method() throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
        byteBuffer = null;
        System.gc(); // 手动 gc
    }

但一旦启用了-XX:+DisableExplicitGC,System.gc()就不会起作用,此时如果应用依赖显式调用来触发GC的话,就不会执行,导致这些DirectByteBuffer对象可能无法及时被回收,从而使得直接内存无法释放。

应用场景

其核心优势在于减少数据在JVM堆和本地内存之间的拷贝次数,适合处理大规模I/O操作和高性能场景,因此主要有以下两个实用场景:

  • 高频网络I/O
  • 大文件处理

1. 高性能网络编程

  • 场景

    • 在Netty框架中,直接内存被广泛用于网络数据缓冲(ByteBuf)。

    • Kafka使用零拷贝技术优化消息传输。

  • 优势

    • 避免数据从堆内存拷贝到本地内存的额外开销。
    • 通过FileChanneltransferTo/transferFrom方法实现“零拷贝”,减少CPU和内存占用。

    针对上面的优势点,我提几个问题来推动理解

    Netty是什么? Netty是一个高性能网络框架,用于快速开发高并发、低延迟的服务器/客户端,帮你处理网络请求的收发

    网络数据缓冲的作用?涉及堆内存和本地内存吗?

    • 作用:临时存储网络传输的数据(如HTTP请求内容),避免因收发速度不匹配导致阻塞。
      • 堆内存缓冲:数据在JVM堆中分配,但Socket发送时需拷贝到本地内存(性能损耗)。
      • 直接内存缓冲(Netty默认):数据直接分配在本地内存,发送时无需额外拷贝,性能更高。

    FileChannel是什么? Java NIO中操作文件的类,支持高效文件读写(如read()/write()方法),常用于大文件处理或零拷贝场景。

    传统读文件像用勺子一勺一勺从锅里(磁盘)舀汤(数据)到碗里(程序),慢!FileChannel直接给锅装个水龙头(文件通道),拧开就能哗啦啦倒汤(高效读写大文件),还能把汤直接倒进卡车(零拷贝)!

    零拷贝是什么? 一种优化技术,减少数据在内核态与用户态之间的冗余拷贝。例如:

    1. 传统方式:文件数据需从磁盘→内核缓冲区→用户缓冲区→Socket缓冲区→网卡。
    2. 零拷贝(如FileChannel.transferTo()):数据直接从内核缓冲区→网卡,跳过用户态拷贝,性能提升显著。

    Kafka的零拷贝技术

    • 用途:优化消息从磁盘到网络的传输过程。
    • 实现
      • 生产者发送消息时,Kafka将消息直接写入磁盘日志文件
      • 消费者拉取消息时,Kafka调用FileChannel.transferTo()将日志文件内容直接从磁盘经内核缓冲区发送到网卡,跳过了用户态的数据拷贝。
  • 示例

    ByteBuf directBuffer = Unpooled.directBuffer(1024); // Netty直接内存分配
    

2. 大文件内存映射

  • 场景:通过FileChannel.map()将文件直接映射到直接内存,适用于大文件读写。

  • 优势

    • 文件操作绕过JVM堆,直接由操作系统管理,避免频繁的read()/write()系统调用。
    • 支持随机访问文件内容,适合数据库、日志处理等场景。

    问题驱动:

    1.什么是大文件内存映射?

    磁盘上的大文件直接转到内存中以便高速读写

    传统读文件: 假设你有一本超厚的书(大文件),每次想读某页内容,都要跑去图书馆(硬盘)翻到那一页,抄到笔记本(内存)上,用完再撕掉笔记本。这样的缺点是反复跑腿(系统调用)、反复抄写(数据拷贝),效率低

    内存映射

    而内存映射通过FileChannel.map()将文件映射到进程的虚拟内存空间,等于直接把整本书(文件)影印到家里墙上(虚拟内存),想读哪页就抬头看墙,不用跑图书馆,也不用抄写,也就是避免了数据在用户态和内核态之间拷贝(零拷贝)。

    2.“支持随机访问文件内容”是什么意思?为何适合数据库、日志处理?

    什么是随机访问?

    • 顺序访问:像磁带听歌,必须从第1首听到第5首,不能直接跳转到第3首。
    • 随机访问:像CD听歌,可以直接跳到第3首、第7首(通过文件偏移量position直接定位)。

    内存映射如何助力随机访问

    将文件映射到内存后,可直接用指针或ByteBufferposition()方法跳转到任意位置,像操作数组一样读写

    3.为何适合数据库处理?

    • 索引查询: 数据库的索引文件存储了“数据位置→磁盘地址”的映射。查询id=100的数据,通过索引直接跳到文件第2048字节读取地址,再跳到对应位置读取数据。此时就需要依赖随机访问来快速定位特定数据块。
    • B+树结构: 数据库索引通常用B+树实现,树的节点分散在文件不同位置,需频繁随机访问。
  • 示例

    // 使用 try-with-resources 语法自动关闭资源,避免手动释放的麻烦
    try (
        // 1. 创建文件通道(FileChannel)用于操作文件
        // Paths.get("largefile.bin"):获取文件路径对象("largefile.bin"为文件名)
        // StandardOpenOption.READ:以只读模式打开文件
        FileChannel channel = FileChannel.open(Paths.get("largefile.bin"), StandardOpenOption.READ)) 
    {
        // 2. 将文件映射到直接内存(内存映射)
        // FileChannel.MapMode.READ_ONLY:映射模式为只读(不可修改文件)
        // 0:映射起始位置(从文件开头开始)
        // channel.size():映射长度(映射整个文件内容)
        MappedByteBuffer mappedBuffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
    
        // 3. 此时 mappedBuffer 可以直接操作文件内容,像操作内存数组一样
        // 例如:
        // mappedBuffer.get(1000);  // 直接读取文件的第1000个字节
        // 无需调用 read(),操作系统会自动处理磁盘与内存的数据同步
    
    } 
    

3. 分布式系统中的跨进程共享内存

  • 场景:多个JVM进程或本地进程通过共享内存通信。

  • 实现

    • 使用直接内存结合内存映射文件(如/dev/shm),实现进程间高效数据交换。

    问题驱动:

    使用直接内存结合内存映射文件实现进程间高效数据交换,具体如何实现?

    一句话让多个进程“直接读写同一块内存”,像共享黑板一样传递数据。

    1.创建共享内存文件

    • 在Linux中,使用/dev/shm目录(内存文件系统,数据不落盘):

      # 创建一个1GB的共享内存文件
      dd if=/dev/zero of=/dev/shm/shared_mem bs=1G count=1
      
    • 在Java中,通过内存映射访问该文件:

      try (FileChannel channel = FileChannel.open(Paths.get("/dev/shm/shared_mem"), StandardOpenOption.READ, StandardOpenOption.WRITE)) {
          MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, channel.size());
      }
      
    1. 进程间读写共享内存:

    • 进程A写入数据:

      buffer.putInt(0, 42);  // 在共享内存起始位置写入整数42
      
    • 进程B读取数据:

      int value = buffer.getInt(0);  // 直接读取到42(无需序列化/反序列化)
      
    • 若需避免并发冲突,可通过信号量(Semaphore)或文件锁(FileLock)协调读写。

    核心优势

    • 零拷贝:数据直接在内存中共享,无需经过Socket或管道(传统IPC需要拷贝数据到内核缓冲区)。
    • 极低延迟:内存访问速度是纳秒级,远超网络或磁盘IO。

    综上,其实有关直接内存的应用都是通过绕过用户态数据拷贝提升性能。

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务