java堆外内存与mmap的关系

643 阅读2分钟

简述

上一篇学习了操作系统的零拷贝技术,趁热打铁,再来学习下java的堆外内存。

Java中文件读操作的过程

  • jvm启动的时候会在用户态申请一块内存,申请的这块内存中有一部分会被称为堆,一般我我们申请的对象就会放在这个堆上,堆上的对象是受gc管理的。
  • 那么除了堆内的内存,其他的内存都被称为堆外内存。在堆外内存中,如果我们是通过Java的DirectByteBuffer申请的,那么这块内存其实也是间接受gc管理的(DirectByteBuffer是通过虚引用来实现堆外内存的释放的,虚引用一般是在 Full GC 时被回收),而如果我们通过jni直接调用c函数申请一块堆外内存,那么这块内存就只能我们自己手动管理了。

当我们在Java中发起一个文件读操作会发生什么呢

Java传统io的方式

  1. 首先内核会将数据从磁盘读到内核缓冲区,
  2. 再从内核拷贝到用户态的堆外内存(这部分是jvm实现)
  3. 然后再将数据从堆外拷贝到堆内。拷贝到堆内其实就是我们在Java中自己手动申请的byte数组中。

我们发现经过了俩次内存拷贝,而nio中只需要使用DirectByteBuffer,就不必将数据从堆外拷贝到堆内了,减少了一次内存拷贝,降低了内存的占用,减轻了gc的压力。

  • 因为Java的IO读写时需要传入地址参数,而受GC影响,堆中的地址往往会发生变化移动,所以Java在进行IO读写时,需要先将数据由内核态复制到堆外内存,然后将堆外内存数据复制到堆内内存,也就是说这个阶段需要经历堆外与堆内的数据拷贝(内核到堆外属于JNI调用,要保证地址的确定性,堆外与堆内数据拷贝时却不一样)

为啥DirectByteBuffer能减少一次拷贝

原因

class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer{
    // 。。。省略
}

具体源码部分就先不去深究了,我们先了解下原因DirectByteBuffer继承自MappedByteBuffer,而MappedByteBuffer是java NIO提供的一个内存映射,底层就是调用Linux mmap()实现的。MappedByteBuffer只能通过调用FileChannel的map()取得,再没有其他方式。FileChannel.map()是抽象方法,具体实现是在 FileChannelImpl.c 可自行查看JDK源码,其map0()方法就是调用了Linux内核的mmap的API。

总结

所以DirectByteBuffer是避免了有堆外内存再向堆内内存拷贝的这个阶段,只是堆内有个引用直接指向堆外内存,所以效率更高。

最后,这只是我个人的理解,如有不对的请指正