马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
1. 媒介
上一篇文章我们先容了 HeapByteBuffer 的源码,这篇文章我们来先容下 MappedByteBuffer,这个 MappedByteBuffer 是 DirectByteBuffer 的父类。
- 【源码剖析】Java NIO 包中的 Buffer
- 【源码剖析】Java NIO 包中的 ByteBuffer
- 【源码剖析】Java NIO 包中的 HeapByteBuffer
2. MappedByteBuffer
MappedByteBuffer 是 ByteBuffer 的子类,体现一个直接字节缓冲区,其内容是文件的内存映射地区。通过 MappedByteBuffer,步伐可以直接对文件内容举行读写操纵,而无需通过传统的 I/O 流或通道。
相比传统的文件 IO 操纵,比如 FileInputStream 和 FileOutputStream,这种方式可以直接对内存中的数据举行操纵,操纵体系会负责将内存中的更改同步到磁盘文件中。
MappedByteBuffer 通过 FileChannel 的 map 方法创建,创建的时间可以设置三种模式:
- MapMode.READ_ONLY:只读模式,映射的缓冲区是只读的
- MapMode.READ_WRITE:读写模式,映射的缓冲区是可读写的,对缓冲区的修改会同步到文件中
- MapMode.PRIVATE:私有模式,映射的缓冲区是可写的,但修改不会同步到文件中,而是创建一个私有副本
但是由于 MappedByteBuffer 利用的是堆外内存,以是如果实行映射过大的文件,大概会导致内存不敷(OutOfMemoryError),毕竟内存映射文件的巨细受操纵体系和可用物理内存的限定。
以是末了总结一下,当须要频仍读写大文件,大概须要随机文件访问的时间就可以利用这个 MappedByteBuffer。
3. 例子
起首我们须要天生一个 1G 的文件。
- public class FileTest {
- public static void main(String[] args) {
- String filePath = "D:\\学习资料\\计算机编程语言java学习\\后台\\JDK源码\\jdk1.8Source\\src\\test\\file\\hello.txt"; // 生成的文件路径
- long fileSizeInBytes = 1024L * 1024 * 1024; // 1GB
- try {
- generateFile(filePath, fileSizeInBytes);
- System.out.println("文件生成成功!路径: " + filePath);
- } catch (IOException e) {
- System.err.println("文件生成失败: " + e.getMessage());
- }
- }
- /**
- * 生成指定大小的文件,内容为 "helloWorld" 的重复填充
- *
- * @param filePath 文件路径
- * @param fileSizeInBytes 文件大小(字节)
- * @throws IOException 如果写入失败
- */
- public static void generateFile(String filePath, long fileSizeInBytes) throws IOException {
- // "helloWorld" 的字节数
- byte[] content = "helloWorld".getBytes();
- int contentLength = content.length;
- try (FileOutputStream fos = new FileOutputStream(filePath);
- BufferedOutputStream bos = new BufferedOutputStream(fos)) {
- // 写入次数
- long len = fileSizeInBytes / contentLength;
- // 一次写入 helloWorld 字节数
- for (long i = 0; i < len; i++) {
- bos.write(content);
- }
- // 剩余字节
- long remainingBytes = fileSizeInBytes % contentLength;
- if (remainingBytes > 0) {
- bos.write(content, 0, (int) remainingBytes);
- }
- }
- }
- }
复制代码 天生了 hello.txt 之后,可以看下面图。

天生 1G 的文件之后我们再来看下传统的 IO 读取数据和 MappedByteBuffer 读取数据的服从。
- public class MappedByteBufferPerformance {
- public static void main(String[] args) throws Exception {
- String filePath = "D:\\学习资料\\计算机编程语言java学习\\后台\\JDK源码\\jdk1.8Source\\src\\test\\file\\hello.txt"; // 生成的文件路径
- long fileSize = 1024 * 1024 * 1024; // 1GB
- long startTime = System.currentTimeMillis();
- try (RandomAccessFile file = new RandomAccessFile(filePath, "r");
- FileChannel channel = file.getChannel()) {
- MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize);
- // 读取文件内容
- while (buffer.hasRemaining()) {
- buffer.get(); // 读取一个字节
- }
- }
- long endTime = System.currentTimeMillis();
- System.out.println("MappedByteBuffer 读取时间: " + (endTime - startTime) + " ms");
- }
- }
复制代码 起首上面是 MappedByteBuffer 的读取,统共用了 317 ms,如下图所示。

下面我们再来看下利用传统 IO 来读取文件的耗时。
- public class BufferedIOPerformance {
- public static void main(String[] args) throws Exception {
- String filePath = "D:\\学习资料\\计算机编程语言java学习\\后台\\JDK源码\\jdk1.8Source\\src\\test\\file\\hello.txt"; // 生成的文件路径
- long startTime = System.currentTimeMillis();
- try (FileInputStream fis = new FileInputStream(filePath);
- BufferedInputStream bis = new BufferedInputStream(fis)) {
- // 读取文件内容
- while (bis.read() != -1) {
- // 读取一个字节
- }
- }
- long endTime = System.currentTimeMillis();
- System.out.println("普通 I/O 读取时间: " + (endTime - startTime) + " ms");
- }
- }
复制代码 平凡 IO 的读取耗时如下:
以是这里总结下读取的结果:
操纵方式文件巨细读取时间备注MappedByteBuffer1 GB317 ms直接内存映射,服从极高BufferedInputStream1 GB19552 ms带缓冲区的平凡 I/O,速率较慢
4. 属性
MappedByteBuffer 中只有一个属性 fd,其他属性都在父类 ByteBuffer 中。
private final FileDescriptor fd; 是 Java 中用于体现操纵体系文件形貌符的对象。它答应 Java 步伐与底层的文件体系举行交互。说白了这玩意就是用来映射文件到内存的。
5. 构造器
- MappedByteBuffer(int mark, int pos, int lim, int cap, // package-private
- FileDescriptor fd)
- {
- super(mark, pos, lim, cap);
- this.fd = fd;
- }
- MappedByteBuffer(int mark, int pos, int lim, int cap) {
- super(mark, pos, lim, cap);
- this.fd = null;
- }
复制代码 这两个构造器实在就是一个指定了文件形貌符,一个没有指定。
6. mappingOffset、mappingAddress、mappingLength
- private long mappingOffset() {
- // 页大小
- int ps = Bits.pageSize();
- // 求直接内存的偏移量
- long offset = address % ps;
- // 确保一定是正数
- return (offset >= 0) ? offset : (ps + offset);
- }
- private long mappingAddress(long mappingOffset) {
- // address 表示缓冲区的起始地址
- // mappingOffset 是上面的偏移量
- return address - mappingOffset;
- }
- private long mappingLength(long mappingOffset) {
- return (long)capacity() + mappingOffset;
- }
复制代码 第一个方法 mappingOffset 获取的是 MappedByteBuffer 的内存所在相对于内存页面起始位置的偏移量, Bits.pageSize(): 这内里返回的是操纵体系的内存分页巨细,一样平常是 4KB 大概 8KB,这里取决于用什么操纵体系。在举行内存映射的时间可以用这个方法求出偏移量来举行内存对齐。
第二个方法 mappingAddress 用来盘算内存页面的起始所在,这里的 mappingOffset 一样平常就是上面的 mappingOffset 方法。address - mappingOffset 这个方法就是利用缓冲区 ByteBuffer 的起始所在减去偏移量。
第三个方法 mappingLength 求出的是内存映射文件的总长度,也就是 mmap 文件映射的内容地区。
上面这几个方法就是获取 MappedByteBuffer 的各种所在信息,那为什么又要有一个偏移量呢?我们知道操纵体系分配最小单位是一个页,以是当利用 mmap 映射操纵体系内存的时间分配的内存总是一个页的起始位置。
固然我们获取了 MappedByteBuffer,但是这个 MappedByteBuffer 的起始位置有大概不是一个页的起始位置,也就是说上面图中 mappingAddress 是页的起始位置,但是 MappedByteBuffer 内里的起始所在是 address。操纵体系分配内存肯定是一个页来分配的,以是 MappedByteBuffer 的起始所在和现实分配的有大概不一样,相差就是 mappingOffset。上面的 mappingOffset 求出来的就是 mappingAddress -> address 之间的间隔,而 mappingAddress 求出来的就是操纵体系内核现实调用 mmap 分配的内存页出发点,就是上图的 mappingAddress,末了一个方法 mappingLength 求出来的就是 mmap 现实分配的内存容量。
7. isLoaded 判定内存是否还在内存中
- public final boolean isLoaded() {
- // 判断 MappedByteBuffer 有没有映射到一个文件
- checkMapped();
- // 如果起始地址为 0 或者容量为 0
- if ((address == 0) || (capacity() == 0))
- // 表示已经不在物理内存里面了
- return true;
- // 获取 mmap 分配的内存的起始位置,也就是图中的 mappingOffset
- long offset = mappingOffset();
- // MappedBuffer 实际映射的内存区域大小 也是 mmap 实际分配的内存大小
- long length = mappingLength(offset);
- // mappingAddress(offset) 获取实际的映射起始位置 mapPosition
- // Bits.pageCount(length) 表示分配了多少个页
- // 调用 native 方法
- return isLoaded0(mappingAddress(offset), length, Bits.pageCount(length));
- }
- private native boolean isLoaded0(long address, long length, int pageCount);
复制代码
- 如果结果是 true,体现缓冲区的内容很大概已经驻留在物理内存中,访问这些数据时不会触发假造内存页错误或 I/O 操纵。
- 如果返回 false,并不肯定体现缓冲区的内容没有驻留在物理内存中,大概只是部分数据不在物理内存中。
8. load 方法将 ByteBuffer 加载到 Page Cache 中
- public final MappedByteBuffer load() {
- // 判断文件描述符是不是空
- checkMapped();
- if ((address == 0) || (capacity() == 0))
- return this;
- // 获取 mmap 内存地址到 MappedByteBuffer 的距离
- long offset = mappingOffset();
- // 获取 mmap 分配的内存长度
- long length = mappingLength(offset);
- // 调用 native 将 MappedByteBuffer 中的内容预读到 page cache 中
- load0(mappingAddress(offset), length);
- // Read a byte from each page to bring it into memory. A checksum
- // is computed as we go along to prevent the compiler from otherwise
- // considering the loop as dead code.
- Unsafe unsafe = Unsafe.getUnsafe();
- // 一个页的大小
- int ps = Bits.pageSize();
- // 这个 ByteBuffer 分配了多少个页
- int count = Bits.pageCount(length);
- // 获取 mmap 映射地址的起始地址
- long a = mappingAddress(offset);
- byte x = 0;
- // 从 mmap 起始地址开始遍历所有页,每遍历一次访问一下都会发生缺页中断,
- // 同时将 MappedByteBuffer 和 Page Cache 进行页表映射
- for (int i=0; i<count; i++) {
- x ^= unsafe.getByte(a);
- a += ps;
- }
- if (unused != 0)
- unused = x;
- return this;
- }
复制代码 这个方法会将 ByteBuffer 内容内里的数据加载到 Page Cache 中,而且这个方法还会遍历全部页预读一次。由于数据加载到 Page Cache 之后,并不会立即就天生假造内存到物理内存的映射。以是加载到 Page Cache 的物理页之后须要访问一次发生缺页停止,这时间才会天生页表项。
9. force 刷盘
- public final MappedByteBuffer force() {
- checkMapped();
- if ((address != 0) && (capacity() != 0)) {
- // 核心逻辑,从 mmap 映射的起始位置开始,将映射的内容进行刷盘
- long offset = mappingOffset();
- force0(fd, mappingAddress(offset), mappingLength(offset));
- }
- return this;
- }
- private native void force0(FileDescriptor fd, long address, long length);
复制代码 这个方法就是将 Buffer 内里的数据举行刷盘。
如有错误,欢迎指出!!!
|