前言
上一篇的博客中,我从底层上分析了BufferInputStream为什么效率会比FileInputStream来得高,这两种方式都是传统的IO,在JDK1.4
中NIO又多了两种新的IO方式:FileChannle 和 MMAP。这里着重来解释FileChannle的高性能的场景。
FileChannle
FileChannel 存在于 java.nio 包中,属于 NIO 的一种,但是注意 NIO 并不一定意味着非阻塞,这里的 FileChannel 就是阻塞的。那么这种新IO较与传统的IO有哪些优势呢?
其实这么说不够严谨,因为你必须用对了FileChannle,不然不存在优势之说。如何用对,就需要先从FileChannle的特点入手,FileChannel 采用了 ByteBuffer 这样的内存缓冲区,让我们可以非常精准的控制写盘的大小,这是普通 IO 无法实现的。
简单来说,传统的IO写数据时,先是往pagecache写入数据,不会马上刷到磁盘,而是将 pagecache 对应的位置标记为脏页,然后内核程序会定时将脏页的数据统一刷到磁盘中,但是为了安全起见,如果突然断电了pagecache中的数据就会丢失,所以这个定时的时间还不能太长,但我们希望的是,每次刷盘的尽可能在脏页比较多的情况下,不然对磁盘的写入效率太低。
而 FileChannel 将ByteBuffer 中的数据也不会马上写入到磁盘中,也是先写到pagecache,但是FileChannel 可以控制写盘的大小,它当然会尽可能写满一块数据块,然后再调用 force() 方法,用于通知操作系统进行及时的刷盘。
所以要想提高FileChannle的效率,必须要了解当前磁盘的数据库大小然后做出写盘大小的调整,这样效率才会快,从这方面也可以看出来FileChannle在读效率上并没有什么优势。
MMAP
关于内存映射,我在前面几篇的博客中已经对其做出了详细的解释,不明白的可以先看一下。
Linux shm和mmap
内存映射 mmap的理解(转载+整理)
RocketMQ 源码分析 消息存储(预备知识二)(转载+整理)
既然已经将了这么多关于内存映射,这里为什么还要再浪费口舌继续解释呢?主要是希望对NIO的方式有一个更加系统的认识。相信大家在网上经常能看到这么一段话来描述MMAP:普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read,write。 当时我是不能理解的,凭什么像访问内存一样速度就快呢?
要知道访问内存就意味着可以使用指针地址来访问了,虽然在Java中没有指针这种说法。那使用地址为什么速度就快了呢?比如你要读取一个文件开头的偏移1000个字节,传统的做法是不是一个for循环遍历到1000个字节,但是现在有了指针,你就可以直接指定position就可以访问了。如果每条实际的消息规定占用10个字节,那么访问的时候,就可以直接使用 文件的起始地址 + n*10 来访问第n条消息了,这也太方便了吧!!
mmap的读:
1 | // 读 |
看到这是我在想,在RMQ中commitlog的偏移量存储在了Consume Queue中,这样是不是就可以利用上面访问内存的特性,根据偏移量快速定位到一条消息呢?只是我的猜测,如果有错,希望有大神能纠正。
同样的也可以指定position进行读写,mmap的读写:
1 | // 写 |
更新2019.2.19
NIO 之 直接内存
下面内容的原文在下方的参考文章给出。
在NIO 中 DirectByteBuffer是Java用于实现堆外内存的一个重要类,我们可以通过该类实现堆外内存的创建、使用和销毁。
DirectByteBuffer该类本身还是位于Java内存模型的堆中。堆内内存是JVM可以直接管控、操纵。而DirectByteBuffer中的unsafe.allocateMemory(size);是个一个native方法,这个方法分配的是堆外内存,通过C的malloc来进行分配的。分配的内存是系统本地的内存,并不在Java的内存中,也不属于JVM管控范围,所以在DirectByteBuffer一定会存在某种方式来操纵堆外内存。
背景知识:
用户进程(位于用户态中)要通过系统调用(Java中即使JNI)来调用内核态中的资源
在linux中内核态的权限是最高的,那么在内核态的场景下,操作系统是可以访问任何一个内存区域的,所以操作系统是可以访问到Java堆的这个内存区域的。
Q:那为什么操作系统不直接访问Java堆内的内存区域了?
这是因为JNI方法访问的内存区域是一个已经确定了的内存区域地质,那么该内存地址指向的是Java堆内内存的话,那么如果在操作系统正在访问这个内存地址的时候,Java在这个时候进行了GC操作,而GC操作会涉及到数据的移动操作[GC经常会进行先标志在压缩的操作。即,将可回收的空间做标志,然后清空标志位置的内存,然后会进行一个压缩,压缩就会涉及到对象的移动,移动的目的是为了腾出一块更加完整、连续的内存空间,以容纳更大的新对象],数据的移动会使JNI调用的数据错乱。所以JNI调用的内存是不能进行GC操作的。
使用堆外内存的原因
1.对垃圾回收停顿的改善。因为full gc 意味着彻底回收,彻底回收时,垃圾收集器会对所有分配的堆内内存进行完整的扫描,这意味着一个重要的事实——这样一次垃圾收集对Java应用造成的影响,跟堆的大小是成正比的。过大的堆会影响Java应用的性能。如果使用堆外内存的话,堆外内存是直接受操作系统管理( 而不是虚拟机 )。这样做的结果就是能保持一个较小的堆内内存,以减少垃圾收集对应用的影响。
2.在某些场景下可以提升程序I/O操纵的性能。少去了将数据从堆内内存拷贝到堆外内存的步骤。