以下全部默认在Linux环境

1.基本原理

1.1虚拟内存

每一个进程OS都会让它认为自己独享全部内存,实际上是不可能的,所以OS采用虚拟内存的方法让进程认为自己独享内存。
image.png

  • 如图,进程P1访问实内存地址A、C、D,但对于它自己来说,是连续地址A、B、C,是OS欺骗了它,
  • 同样,进程P2访问实内存地址B、C对于它自己来说是A、B
  • 虚拟空间对于程序来说(也对于程序员来说)是一段和物理内存相同大小(或自定义,比如JVM自定义堆大小等),连续的地址空间(还没分配内存,所以叫地址空间,预留的空间,还不能读写数据)
  • 虚拟内存和物理内存都是划分成同样单位的页,默认1页=4KByte

    1.2mmap

    mmap是针对于文件的,是让进程在虚拟内存当中读写磁盘文件的一种方式。

    首先,关于文件读写,我们有传统的IO方式:

    image.png
  • 用户态读取核态的数据缓存,数据缓存从内存or磁盘读取数据
  • 因为用户态是被核态屏蔽的,所以要想读取数据,核态要将数据主动copy给用户态:

image.png

其次,看看mmap的方式:

image.png

  • 用户态通过mmap方法(函数、算法、随便啦)获取到指针(只想文件首部地址,就像字符串的char*一样)
  • 将指针(加偏移量)地址传给核态,核态根据将缺页的数据从磁盘读到内存当中
  • 指针通过mmap映射到真实内存地址,用户态直接读取器内存相应地址的数据

image.png

  • 也就是说,mmap比传统IO少了内核将数据copy给用户态的过程,全程核态、用户态都根据mmap映射到内存同一位置

    2. 实现方式

    2.1 C/C++

    mmap是Linux内存(虚拟内存)的一种机制,其内核是C++编写,自然C/C++实现就较为简单,因为在 Include/sys/mman.h当中实现了mmap ```c

    include

    include

    include

    include

    include

    using namespace std;

int main() { int fd = 0; char *ptr = NULL; struct stat buf = {0};

  1. char filePath[]="mmapTestFile";
  2. //use io.h open file ususally return 3(means regular read write) or -1(means fail)
  3. if ((fd = open(filePath, O_RDWR)) < 0)
  4. {
  5. printf("open file error\n");
  6. return -1;
  7. }
  8. //get file state,file state include meta data of file for example:st_size[file length]
  9. if (fstat(fd, &buf) < 0)
  10. {
  11. printf("get file state error:%d\n", errno);
  12. close(fd);
  13. return -1;
  14. }
  15. //mmap just like fopen() but return a ptr point the address of file head
  16. ptr = (char *)mmap(NULL, buf.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
  17. if (ptr == MAP_FAILED)
  18. {
  19. printf("mmap failed\n");
  20. close(fd);
  21. return -1;
  22. }
  23. close(fd);
  24. printf("length of the file is : %d\n", buf.st_size);
  25. printf("the %s content is : %s\n", filePath, ptr);
  26. //replace the 3rd char in the file by ptr like use an array
  27. ptr[3] = 'a';
  28. printf("the %s new content is : %s\n", filePath, ptr);
  29. //munmap just like close()
  30. munmap(ptr, buf.st_size);
  31. return 0;

}

  1. 注释已解释,此处不再赘述
  2. <a name="7qjfx"></a>
  3. ### 2.2 JAVA
  4. 由于Java本身运行在JVM之上,离OS较远,通常使用第三方库,调用nativeby C++)去实现<br />不过,JAVA nio有类似方法
  5. <a name="sNhs2"></a>
  6. #### 2.2.1 NIO:JDK自带的MappedByteBuffer
  7. 重中之重:这是JDKNIO包下FileChannel的一个“实现”,在这里先说明,其实第三方开源库都是基于这个而实现的<br />![image-20200922150055102.png](https://cdn.nlark.com/yuque/0/2020/png/358297/1600758293831-f89fb0b1-f6fa-4af4-a013-e8672acd121c.png#align=left&display=inline&height=371&margin=%5Bobject%20Object%5D&name=image-20200922150055102.png&originHeight=371&originWidth=582&size=91884&status=done&style=none&width=582)<br />可以看出,MappedByteBuffer继承并实现了ByteBuffer-->Buffer,然后再实际使用中,我们真正用到的是DirectBuffer,这是再继承MappedByteBuffer并实现自己的接口的类
  8. 同时还有个HeapByteBuffer,两者的区别就是:
  9. - MappedByteBufferFileChannel通过native方法map0实现的,和C一样的mmap,意味着映射地址所在虚拟空间在系统内存里面(JVM之外)
  10. - HeapByteBuffer则是类似的操作,但是映射地址所在虚拟空间在JVM内(堆内存)里面
  11. 实现:
  12. ```java
  13. public class FileChannleMap {
  14. public final MappedByteBuffer mappedByteBuffer;
  15. private final FileChannel fc;
  16. @SneakyThrows
  17. public FileChannleMap(File file, long capacity) {
  18. final long fsize = file.length();
  19. //File 创建RandomAccessFile,然后创建FileChannel
  20. fc = new RandomAccessFile(file, "rw").getChannel();
  21. final long l = capacity > 0 ? capacity : fc.size() * 2;
  22. //通过FileChannel的map方法就得到了MappedByteBuffer(DirectByteBuffer)
  23. mappedByteBuffer = fc.map(FileChannel.MapMode.READ_WRITE, 0, l);
  24. //load方法将文件加载到虚拟内存(创建的时候会加载的,如果没出错可以不加这句)
  25. mappedByteBuffer.load();
  26. }
  27. public void writeByte(byte[] bytes) {
  28. mappedByteBuffer.rewind();
  29. mappedByteBuffer.put(bytes);
  30. }
  31. public void writeText(String text) {
  32. this.writeByte(text.getBytes());
  33. }
  34. @SneakyThrows
  35. public String getAll() {
  36. mappedByteBuffer.rewind();
  37. final byte[] buff = new byte[mappedByteBuffer.limit()];
  38. mappedByteBuffer.get(buff);
  39. return new String(buff, StandardCharsets.UTF_8);
  40. }
  41. @SneakyThrows
  42. public String getPart(int offset, int len) {
  43. mappedByteBuffer.rewind();
  44. final byte[] buf = new byte[len];
  45. mappedByteBuffer.get(buf, offset, len);
  46. return new String(buf, StandardCharsets.UTF_8);
  47. }
  48. public Character getChar(int pos) {
  49. return (char) mappedByteBuffer.get(pos);
  50. }
  51. public void clearAll() {
  52. mappedByteBuffer.clear();
  53. }
  54. @SneakyThrows
  55. public void close() {
  56. mappedByteBuffer.force();
  57. if (fc != null && fc.isOpen())
  58. fc.close();
  59. }
  60. }
  • API:参见BufferAPI
  • 这里所有操作是基于byte的,之后自己再转换成String或其他类型,注意,一个char是2Byte,int是4Byte
  • map方法第三个参数非常重要,是你开辟的内存的空间大小,是不可更改的!如果设置不合理很容易发生BufferOutBoundsException(or BufferUnderBoundsException)
  • MappedByteBuffer实现的方法和C的mmap基本一致,比如C通过指针的数组操作ptr[i]进行读或写,MappedByteBuffer则有对应方法get(index)、get(&byte[],offset.len),put(byte[])、put(index,byte)、put(byte[],offset,len)

2.2.2 fengzhizi715/bytekit(GitHub)

这个包是对java byte数据类型的一个封装(bytekit-core),同时对MappedByteBuffer一个封装(bytekit-mmap)。

  1. private MmapBuffer buffer = null;
  2. private String file;
  3. private int position = 0; // current the position for reader
  4. public MmapBytes(String file,Long mapSize) {
  5. this.file = file;
  6. this.buffer = new MmapBuffer(file,mapSize);
  7. System.out.println("initializer with " + mapSize + " bytes map buffer");
  8. }

下面简单介绍下比较有用的几个封装:

  • 扩容

    1. public void remap(Long mapSize) {
    2. ByteBuffer byteBuffer = Utils.cloneByteBuffer(buffer.getMappedByteBuffer());
    3. buffer.getMappedByteBuffer().clear();
    4. free();
    5. this.buffer = new MmapBuffer(file,mapSize);
    6. try {
    7. writeBytes(byteBuffer.array());
    8. } catch (Exception e) {
    9. e.printStackTrace();
    10. }
    11. System.out.println("re-map with " + mapSize + " bytes map buffer");
    12. }

    在上面有说到,初始化内存的空间大小时不可更改的,那如果太小怎么办呢?
    这里的扩容和JavaCollections差不多的,通过将Buffer(虚拟内存)拷贝,然后用源文件重新开辟新空间,再将拷贝的数据写进去。

  • 释放

    1. private void unmap(MappedByteBuffer mbb) {
    2. if (mbb == null) {
    3. return;
    4. }a
    5. try {
    6. Class<?> clazz = Class.forName("sun.nio.ch.FileChannelImpl");
    7. Method m = clazz.getDeclaredMethod("unmap", MappedByteBuffer.class);
    8. m.setAccessible(true);
    9. m.invoke(null, mbb);
    10. } catch (Throwable e) {
    11. e.printStackTrace();
    12. }
    13. }

    在C当中,unmmap可以解除映射,而在JAVA当中,FileChannelImpl将其和map0一样私有封装了起来,所以这里采用反射机制,暴力解除私有限制,调用umap解除映射。

    2.2.3 odnoklassniki/one-nio(Github)