深入浅出MappedByteBuffer(推荐)

目录
  • 1.内存管理
  • 2.MappedByteBuffer的深度剖析
    • 2.1 map过程
    • 2.2 get过程
  • 3.性能分析
  • 4.总结

java io操作中通常采用BufferedReaderBufferedInputStream等带缓冲的IO类处理大文件,不过java nio中引入了一种基于MappedByteBuffer操作大文件的方式,其读写性能极高,本文会介绍其性能如此高的内部实现原理。

在深入MappedByteBuffer之前,先看看计算机内存管理的一些知识:

1.内存管理

  • MMC:CPU的内存管理单元。
  • 物理内存:即内存条的内存空间。
  • 虚拟内存:计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
  • 页面文件:操作系统反映构建并使用虚拟内存的硬盘空间大小而创建的文件,在windows下,即pagefile.sys文件,其存在意味着物理内存被占满后,将暂时不用的数据移动到硬盘上。
  • 缺页中断:当程序试图访问已映射在虚拟地址空间中但未被加载至物理内存的一个分页时,由MMC发出的中断。如果操作系统判断此次访问是有效的,则尝试将相关的页从虚拟内存文件中载入物理内存。

那么问题来了,为什么会有虚拟内存和物理内存的区别?

如果正在运行的一个进程,它所需的内存是有可能大于内存条容量之和的,如内存条是256M,程序却要创建一个2G的数据区,那么所有数据不可能都加载到内存(物理内存),必然有数据要放到其他介质中(比如硬盘),待进程需要访问那部分数据时,再调度进入物理内存。

什么是虚拟内存地址和物理内存地址?

假设你的计算机是32位,那么它的地址总线是32位的,也就是它可以寻址00xFFFFFFFF(4G)的地址空间,但如果你的计算机只有256M的物理内存0x0x0FFFFFFF(256M),同时你的进程产生了一个不在这256M地址空间中的地址,那么计算机该如何处理呢?回答这个问题前,先说明计算机的内存分页机制

计算机会对虚拟内存地址空间(32位为4G)进行分页产生页(page),对物理内存地址空间(假设256M)进行分页产生页帧(page frame),页和页帧的大小一样,所以虚拟内存页的个数势必要大于物理内存页帧的个数。

在计算机上有一个页表(page table),就是映射虚拟内存页到物理内存页的,更确切的说是页号到页帧号的映射,而且是一对一的映射。

问题来了,虚拟内存页的个数 > 物理内存页帧的个数,岂不是有些虚拟内存页的地址永远没有对应的物理内存地址空间?

答案肯定是否定的,操作系统是这样处理的。
操作系统有个页面失效(page fault)功能。操作系统找到一个最少使用的页帧,使之失效,并把它写入磁盘,随后把需要访问的页放到页帧中,并修改页表中的映射,保证了所有的页都会被调度。

我们简单总结一下上面的内容

虚拟内存地址:由页号(与页表中的页号关联)和偏移量(页的小大,即这个页能存多少数据)组成。

举个例子,有一个虚拟地址它的页号是4,偏移量是20,那么他的寻址过程是这样的:

首先到页表中找到页号4对应的页帧号(比如为8),如果页不在内存中,则用失效机制调入页,接着把页帧号和偏移量传给MMC组成一个物理上真正存在的地址,最后就是访问物理内存的数据了。

2.MappedByteBuffer的深度剖析

从继承结构上看,MappedByteBuffer继承自ByteBuffer,内部维护了一个逻辑地址address
我们通过一个简单的示例看看MappedByBuffer是怎么使用的

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Scanner;

public class MappedByteBufferTest {
    public static void main(String[] args) {
        File file = new File("D://data.txt");
        long len = file.length();
        byte[] ds = new byte[(int) len];

        try {
            MappedByteBuffer mappedByteBuffer = new RandomAccessFile(file, "r")
                    .getChannel()
                    .map(FileChannel.MapMode.READ_ONLY, 0, len);
            for (int offset = 0; offset < len; offset++) {
                byte b = mappedByteBuffer.get();
                ds[offset] = b;
            }

            Scanner scan = new Scanner(new ByteArrayInputStream(ds)).useDelimiter(" ");
            while (scan.hasNext()) {
                System.out.print(scan.next() + " ");
            }

        } catch (IOException e) {
        }
    }
}

上面代码是不是看的不是很明白,我逐一给大家解释一下

整个代码其实就包含了两个过程:

  • map过程
  • get过程

2.1 map过程

FileChannel提供了map方法把文件映射到虚拟内存,通常情况可以映射整个文件,如果文件比较大,可以进行分段映射。

FileChannel中的几个变量:

  • MapMode mode:内存映像文件访问的方式,共三种
  • MapMode.READ_ONLY:只读,试图修改得到的缓冲区将导致抛出异常。
  • MapMode.READ_WRITE:读/写,对得到的缓冲区的更改最终将写入文件;但该更改对映射到同一文件的其他程序不一定是可见的。
  • MapMode.PRIVATE:私用,可读可写,但是修改的内容不会写入文件,只是buffer自身的改变,这种能力称之为”copy on write”。
  • position:文件映射时的起始位置
  • allocationGranularity::Memory allocation size for mapping buffers,通过native函数initIDs初始化。、

因为MappedByteBuffer做的实在是太过于优秀了,所以我不得不带着大家读一下源码,看看他们的设计思想和思路

1.通过RandomAccessFile获取FileChannel

public final FileChannel getChannel() {
    synchronized (this) {
        if (channel == null) {
            channel = FileChannelImpl.open(fd, path, true, rw, this);
        }
        return channel;
    }
}

上述实现可以看出,由于synchronized ,只有一个线程能够初始化FileChannel。

2.通过FileChannel.map方法,把文件映射到虚拟内存,并返回逻辑地址address,实现如下:

public MappedByteBuffer map(MapMode mode, long position, long size)  throws IOException {
        int pagePosition = (int)(position % allocationGranularity);
        long mapPosition = position - pagePosition;
        long mapSize = size + pagePosition;
        try {
            addr = map0(imode, mapPosition, mapSize);
        } catch (OutOfMemoryError x) {
            System.gc();
            try {
                Thread.sleep(100);
            } catch (InterruptedException y) {
                Thread.currentThread().interrupt();
            }
            try {
                addr = map0(imode, mapPosition, mapSize);
            } catch (OutOfMemoryError y) {
                // After a second OOME, fail
                throw new IOException("Map failed", y);
            }
        }
        int isize = (int)size;
        Unmapper um = new Unmapper(addr, mapSize, isize, mfd);
        if ((!writable) || (imode == MAP_RO)) {
            return Util.newMappedByteBufferR(isize,
                                             addr + pagePosition,
                                             mfd,
                                             um);
        } else {
            return Util.newMappedByteBuffer(isize,
                                            addr + pagePosition,
                                            mfd,
                                            um);
        }
}

上述代码可以看出,最终map通过native函数map0完成文件的映射工作。

  • 如果第一次文件映射导致OOM,则手动触发垃圾回收,休眠100ms后再次尝试映射,如果失败,则抛出异常。
  • 通过newMappedByteBuffer方法初始化MappedByteBuffer实例,不过其最终返回的是DirectByteBuffer的实例,实现如下:
static MappedByteBuffer newMappedByteBuffer(int size, long addr, FileDescriptor fd, Runnable unmapper) {
    MappedByteBuffer dbb;
    if (directByteBufferConstructor == null)
        initDBBConstructor();
    dbb = (MappedByteBuffer)directByteBufferConstructor.newInstance(
          new Object[] { new Integer(size),
                         new Long(addr),
                         fd,
                         unmapper }
    return dbb;
}
// 访问权限
private static void initDBBConstructor() {
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            Class<?> cl = Class.forName("java.nio.DirectByteBuffer");
                Constructor<?> ctor = cl.getDeclaredConstructor(
                    new Class<?>[] { int.class,
                                     long.class,
                                     FileDescriptor.class,
                                     Runnable.class });
                ctor.setAccessible(true);
                directByteBufferConstructor = ctor;
        }});
}

由于FileChannelImpl和DirectByteBuffer不在同一个包中,所以有权限访问问题,通过AccessController类获取DirectByteBuffer的构造器进行实例化。

DirectByteBuffer是MappedByteBuffer的一个子类,其实现了对内存的直接操作。

2.2 get过程

MappedByteBuffer的get方法最终通过DirectByteBuffer.get方法实现的。

public byte get() {
    return ((unsafe.getByte(ix(nextGetIndex()))));
}
public byte get(int i) {
    return ((unsafe.getByte(ix(checkIndex(i)))));
}
private long ix(int i) {
    return address + (i << 0);
}

map0()函数返回一个地址address,这样就无需调用readwrite方法对文件进行读写,通过address就能够操作文件。

底层采用unsafe.getByte方法,通过(address + 偏移量)获取指定内存的数据。

第一次访问address所指向的内存区域,导致缺页中断,中断响应函数会在交换区中查找相对应的页面,如果找不到(也就是该文件从来没有被读入内存的情况),则从硬盘上将文件指定页读取到物理内存中(非jvm堆内存)。

如果在拷贝数据时,发现物理内存不够用,则会通过虚拟内存机制(swap)将暂时不用的物理页面交换到硬盘的虚拟内存中。

3.性能分析

从代码层面上看,从硬盘上将文件读入内存,都要经过文件系统进行数据拷贝,并且数据拷贝操作是由文件系统硬件驱动实现的,理论上来说,拷贝数据的效率是一样的。

但是通过内存映射的方法访问硬盘上的文件,效率要比read和write系统调用高,这是为什么?

read()是系统调用,首先将文件从硬盘拷贝到内核空间的一个缓冲区,再将这些数据拷贝到用户空间,实际上进行了两次数据拷贝;

map()也是系统调用,但没有进行数据拷贝,当缺页中断发生时,直接将文件从硬盘拷贝到用户空间,只进行了一次数据拷贝。

所以,采用内存映射的读写效率要比传统的read/write性能高。

4.总结

MappedByteBuffer使用虚拟内存,因此分配(map)的内存大小不受JVM的-Xmx参数限制,但是也是有大小限制的。

如果当文件超出1.5G限制时,可以通过position参数重新map文件后面的内容。

MappedByteBuffer在处理大文件时的确性能很高,但也存在一些问题,如:内存占用、文件关闭不确定,被其打开的文件只有在垃圾回收的才会被关闭,而且这个时间点是不确定的

javadoc中也提到:

A mapped byte buffer and the file mapping that it represents remain valid until the buffer itself is garbage-collected.*

翻译过来便是:

映射的字节缓冲区及其表示的文件映射在缓冲区本身被垃圾收集之前保持有效

到此这篇关于深入浅出MappedByteBuffer的文章就介绍到这了,更多相关MappedByteBuffer简介内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入浅出MappedByteBuffer(推荐)

    目录 1.内存管理 2.MappedByteBuffer的深度剖析 2.1 map过程 2.2 get过程 3.性能分析 4.总结 java io操作中通常采用BufferedReader,BufferedInputStream等带缓冲的IO类处理大文件,不过java nio中引入了一种基于MappedByteBuffer操作大文件的方式,其读写性能极高,本文会介绍其性能如此高的内部实现原理. 在深入MappedByteBuffer之前,先看看计算机内存管理的一些知识: 1.内存管理 MMC:C

  • Spring Boot超大文件上传实现秒传功能

    目录 1.分片上传 1.1 什么是分片上传 1.2 分片上传的场景 2.断点续传 2.1 什么是断点续传 2.2 应用场景 2.3 实现断点续传的核心逻辑 2.4 实现流程步骤 3.分片上传/断点上传代码实现 3.1 前端实现 3.2 后端写入文件 3.3 进行写入操作的核心代码 4.秒传 4.1 什么是秒传 4.2 实现的秒传核心逻辑 4.3 核心代码 5.总结 文件上传是一个老生常谈的话题了,在文件相对比较小的情况下,可以直接把文件转化为字节流上传到服务器,但在文件比较大的情况下,用普通的方

  • 干货 | Linux新手入门好书推荐

    前言 经常有读者问小编可否推荐一些 Linux 入门书籍,正好最近在知乎也看到类似的问题,如几个零碎的命令难以在 Linux 环境中存活,所以如果要真正形成自己的知识体系,还是要靠阅读专业书籍来积累. 众所周知Linux 对后端开发是必备技能,对 Python 开发者来说重要性不言而喻,将来你写的每一行代码,都有可能在 Linux 环境中运行.前端开发是否有必要学习 Linux 呢?这个就好比学驾照,学到了,总有一天会给你带来便利,暂时没时间学的可以先收藏着. linux之路,路漫漫其修远兮,吾

  • 深入浅出webpack教程系列_安装与基本打包用法和命令参数详解

    webpack,我想大家应该都知道或者听过,Webpack是前端一个工具,可以让各个模块进行加载,预处理,再进行打包.现代的前端开发很多环境都依赖webpack构建,比如vue官方就推荐使用webpack.废话不多说,我们赶紧开始吧. 第一步.安装webpack 新建文件夹webpack->再在webpack下面新建demo->命令行切换到demo目录,使用npm init --yes 初始化项目的package.json文件,然后执行npm install webpack --save-de

  • TIOBE编程语言排行榜前20的语言入门书籍推荐

    根据TIOBE 编程语言排行榜前20的语言分享相关图书(部分空缺). 在正式介绍编程语言排行之前,你敢不敢先挑战一下自己的编程技能?!测试下自己的编程风格?! 挑战编程技能:57道程序员功力测试题 践行"实践出真知"的理念,从基本原理出发解决问题 新手程序员在具备了理论基础后,面对实际项目时往往不知道如何解决问题:有经验的程序员在学习了一门新语言后,也会有很多不知道如何使用的特性.针对程序员的这一普遍困惑,资深软件工程师Brian P. Hogan在这本书中总结了57道练习题,帮助他们

  • 深入浅出讲解Java集合之Collection接口

    目录 一.集合框架的概述 二.集合框架(Java集合可分为Collection 和 Map 两种体系) 三.Collection接口中的方法的使用 四.集合元素的遍历操作 A. 使用(迭代器)Iterator接口 B. jdk5.0新增foreach循环,用于遍历集合.数组 五.Collection子接口之一:List接口 List接口方法 ArrayList的源码分析: JDK 7情况下: JDK 8中ArrayList的变化: LinkedList的源码分析: Vector的源码分析: 六.

  • Java深入浅出掌握SpringBoot之MVC自动配置原理篇

    Spring Boot 为 Spring MVC 提供了自动配置,适用于大多数应用程序. 官方文档描述: 自动配置在 Spring 的默认值之上添加了以下功能: 从官方描述解析: If you want to keep Spring Boot MVC features and you want to add additionalMVC configuration (interceptors, formatters, view controllers, and other features), y

  • 深入浅出探索Java分布式锁原理

    目录 什么是分布式锁?它能干什么? 分布式锁实现方案 基于数据库的分布式锁实现方案 实现原理 方案分析 基于Redis的分布式锁实现方案 基于sentnx命令的实现原理 方案分析 基于Redisson实现 RedLock 方案分析 基于Zookeeper的分布式锁实现方案 实现原理 方案分析 分布式锁方案到底选哪个? 总结 什么是分布式锁?它能干什么? 相信大家对于Java提供的synchronized关键字以及Lock锁都不陌生,在实际的项目中大家都使用过.如下图所示,在同一个JVM进程中,T

  • C++ 深入浅出探索模板

    目录 非类型模板参数 模板特化 函数模板特化 类模板特化 全特化 偏特化 模板分离编译 模板的分离编译 解决方法 总结 非类型模板参数 模板参数分类类型形参与非类型形参. 类型形参:出现在模板参数列表中,跟在class或者typename之类的参数类型名称. 非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用. 注意: 浮点数,类对象以及字符串是不允许作为非类型模板的. 非类型的模板参数必须在编译期就能确认结果. 模板特化 有时候,编译默认函数模板

  • redis深入浅出分布式锁实现上篇

    目录 问题描述 解决方案 编写代码 优化之UUID防误删 问题描述 随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程.多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力.为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题! 分布式锁主流的实现方案: 1. 基于数据库实现分布式锁 2. 基于缓存(Redis等) 3. 基于Zookeeper 每一种分布

随机推荐