相关java IO专题
mmap的作用
mmap的作用,是将文件的一部分直接映射到内存(堆外内存),对这个映射的操作会由操作系统在某个特定的时期自动将脏页写回文件对应的位置(也可以通过msync强制写回),而不必调用read/write。mapp完成后,OS并没有直接读取文件的内容,而是在真正要访问的时候,通过缺页异常来进行读磁盘操作。
mmap相比普通的文件读写优势在哪
常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。
而mmap实现了将设备驱动在内核空间的部分地址直接映射到用户空间,使得用户程序可以直接访问和操作相应的内容,减少了额外的拷贝。
说白了,mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。因此mmap效率更高。
mmap不适合的场景
mmap也不是万能的:
mmap内存映射的大小始终是整数页,因此对于文件实际大小和映射的空间之间多少回有差异,这个差异的空间是被浪费的,对于小文件来说这个浪费比例被放大,因此mmap更适合大文件。
频繁映射大量不同大小的内存,会导致内存碎片化。
java中mmap的应用
java中提供了MappedByteBuffer支持mmap的调用,其本身是一个DirectByteBuffer,即堆外内存。获取MappedByteBuffer方式如下:
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileSize);
复制代码
另外MappedByteBuffer扩展了一个force方法,强制将buffer中的内容写入磁盘。
java内存映射的应用场景
消息队列的场景非常契合前面说到的mmap的优势,并且能够完美避开其劣势。java开源消息队列RocketMQ和QMQ都采用了MappedByteBuffer写消息数据,以顺序写的方式提高吞吐量。这里举QMQ为例(RocketMQ笔者不太了解hhh):
所有的消息都用LogSegment来管理,一个LogSegment对应磁盘上的一个文件,持有一个fileChannel和
mappedByteBuffer,并且每次都会记录最后写入的位置AtomicInteger wrotePosition = new AtomicInteger(0);
mappedByteBuffer由channel.map获得,fileSize默认都是1G。
写数据的时候会通过mappedByteBuffer.slice获取targetByteBuffer(没有直接写到mappedByteBuffer是因为??),并通过 buffer.position(currentPos);将写指针移动到上次写的位置,然后将数据追加到后面:
//这里的workingBuffer是堆内存,初始化了一些header数据
targetBuffer.put(workingBuffer.array(), 0, headerSize);
//这里的nioBuffer是netty接收到的消息,默认为堆外内存
targetBuffer.put(message.getBody().nioBuffer());
复制代码
另外在PeriodicFlushService中开启一个定时任务,每隔一定时间(默认500ms)将buffer数据刷盘(通过buffer.force())。
参考文章