【Java深层系列】「并发编程系列」深入分析和研究MappedByteBuffer的实现原理和开发指南

简介: 【Java深层系列】「并发编程系列」深入分析和研究MappedByteBuffer的实现原理和开发指南

前言介绍


在Java编程语言中,操作文件IO的时候,通常采用BufferedReader,BufferedInputStream等带缓冲的IO类处理大文件,不过java nio中引入了一种基于MappedByteBuffer操作大文件的方式,其读写性能极高,比起bio的模型处理方式,它大大的加大了支持解析读取文件的数量和空间。



OS的内存管理


内存层面的技术名词概念


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




虚拟内存和物理内存


正在运行的一个进程,它所需的内存是有可能大于内存条容量之和的,如内存条是256M,程序却要创建一个2G的数据区,那么所有数据不可能都加载到内存(物理内存),必然有数据要放到其他介质中(比如硬盘),待进程需要访问那部分数据时,再调度进入物理内存,而这种场景下,被调度到硬盘的资源空间所占用的存储,我们便将他理解为虚拟内存。




MappedByteBuffer


从大体上讲一下MappedByteBuffer 究竟是什么。从继承结构上来讲,MappedByteBuffer 继承自 ByteBuffer,所以,ByteBuffer 有的能力它全有;像变动 position 和 limit 指针啦、包装一个其他种类Buffer的视图啦,内部维护了一个逻辑地址address。



“MappedByteBuffer” 会提升速度,变快


  • 为什么快?因为它使用 direct buffer 的方式读写文件内容,这种方式的学名叫做内存映射。这种方式直接调用系统底层的缓存,没有 JVM 和系统之间的复制操作,所以效率大大的提高了。而且由于它这么快,还可以用它来在进程(或线程)间传递消息,基本上能达到和 “共享内存页” 相同的作用,只不过它是依托实体文件来运行的。


  • 还有就是它可以让读写那些太大而不能放进内存中的文件。实现假定整个文件都放在内存中(实际上,大文件放在内存和虚拟内存中),基本上都可以将它当作一个特别大的数组来访问,这样极大的简化了对于大文件的修改等操作。



MappedByteBuffer的案例用法


FileChannel 提供了 map 方法来把文件映射为 MappedByteBuffer: MappedByteBuffer map(int mode,long position,long size); 可以把文件的从 position 开始的 size 大小的区域映射为 MappedByteBuffer,mode 指出了可访问该内存映像文件的方式,共有三种,分别为:


  • MapMode.READ_ONLY(只读): 试图修改得到的缓冲区将导致抛出 ReadOnlyBufferException。


  • MapMode.READ_WRITE(读 / 写): 对得到的缓冲区的更改最终将写入文件;但该更改对映射到同一文件的其他程序不一定是可见的(无处不在的 “一致性问题” 又出现了)。


  • MapMode.PRIVATE(专用): 可读可写, 但是修改的内容不会写入文件, 只是 buffer 自身的改变,这种能力称之为”copy on write”



MappedByteBuffer较之ByteBuffer新增的三个方法


  • fore() 缓冲区是 READ_WRITE 模式下,此方法对缓冲区内容的修改强行写入文件


  • load() 将缓冲区的内容载入内存,并返回该缓冲区的引用


  • isLoaded() 如果缓冲区的内容在物理内存中,则返回真,否则返回假



采用FileChannel构建相关的MappedByteBuffer

//一个byte占1B,所以共向文件中存128M的数据
int length = 0x8FFFFFF;
try (FileChannel channel = FileChannel.open(Paths.get("src/c.txt"),
    StandardOpenOption.READ, StandardOpenOption.WRITE);) {
  MappedByteBuffer mapBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, length);
  for(int i=0;i<length;i++) {
    mapBuffer.put((byte)0);
  }
  for(int i = length/2;i<length/2+4;i++) {
     //像数组一样访问
     System.out.println(mapBuffer.get(i));
  }
}
复制代码


实现相关的读写文件的对比处理
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class TestMappedByteBuffer {
  private static int length = 0x2FFFFFFF;//1G
  private abstract static class Tester {
    private String name;
    public Tester(String name) {
      this.name = name;
    }
    public void runTest() {
      System.out.print(name + ": ");
      long start = System.currentTimeMillis();
      test();
      System.out.println(System.currentTimeMillis()-start+" ms");
    }
    public abstract void test();
  }
  private static Tester[] testers = {
      new Tester("Stream RW") {
      public void test() {
        try (FileInputStream fis = new FileInputStream(
            "src/a.txt");
            DataInputStream dis = new DataInputStream(fis);
            FileOutputStream fos = new FileOutputStream(
                "src/a.txt");
            DataOutputStream dos = new DataOutputStream(fos);) {
          byte b = (byte)0;
          for(int i=0;i<length;i++) {
            dos.writeByte(b);
            dos.flush();
          }
          while (dis.read()!= -1) {
          }
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    },
    new Tester("Mapped RW") {
      public void test() {
        try (FileChannel channel = FileChannel.open(Paths.get("src/b.txt"),
            StandardOpenOption.READ, StandardOpenOption.WRITE);) {
          MappedByteBuffer mapBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, length);
          for(int i=0;i<length;i++) {
            mapBuffer.put((byte)0);
          }
          mapBuffer.flip();
          while(mapBuffer.hasRemaining()) {
            mapBuffer.get();
          }
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    },
    new Tester("Mapped PRIVATE") {
      public void test() {
        try (FileChannel channel = FileChannel.open(Paths.get("src/c.txt"),
            StandardOpenOption.READ, StandardOpenOption.WRITE);) {
          MappedByteBuffer mapBuffer = channel.map(FileChannel.MapMode.PRIVATE, 0, length);
          for(int i=0;i<length;i++) {
            mapBuffer.put((byte)0);
          }
          mapBuffer.flip();
          while(mapBuffer.hasRemaining()) {
            mapBuffer.get();
          }
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
  };
  public static void main(String[] args) {
    for(Tester tester:testers) {
      tester.runTest();
    }
  }
}
复制代码



测试结果


  • Stream RW->用传统流的方式,最慢,应该是由于用的数据量是 1G,无法全部读入内存,所以它根本无法完成测试。


  • MapMode.READ_WRITE,它的速度每次差别较大,在 0.6s 和 8s 之间波动,而且很不稳定。


  • MapMode.PRIVATE就稳得出奇,一直是 1.1s 到 1.2s 之间。


无论是哪个速度都是十分惊人的,但是 MappedByteBuffer 也有不足,就是在数据量很小的时候,表现比较糟糕,那是因为 direct buffer 的初始化时间较长,所以建议大家只有在数据量较大的时候,在用 MappedByteBuffer。



map过程


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


FileChannel中的几个变量:


  • MapMode mode:内存映像文件访问的方式,也就是上面说的三种方式。
  • position:文件映射时的起始位置。
  • allocationGranularity:Memory allocation size for mapping buffers,通过native函数initIDs初始化。


接下去通过分析源码,了解一下map过程的内部实现。通过RandomAccessFile获取FileChannel。

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

上述实现可以看出,由于synchronized ,只有一个线程能够初始化FileChannel。通过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完成文件的映射工作。


  1. 如果第一次文件映射导致OOM,则手动触发垃圾回收,休眠100ms后再次尝试映射,如果失败,则抛出异常。


  1. 通过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的一个子类,其实现了对内存的直接操作。




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,这样就无需调用read或write方法对文件进行读写,通过address就能够操作文件。底层采用unsafe.getByte方法,通过(address + 偏移量)获取指定内存的数据。


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


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




性能分析


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


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


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


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


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


采用RandomAccessFile构建相关的MappedByteBuffer


通过MappedByteBuffer读取文件

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) {}
    }
}
复制代码




总结


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.*




参考资料


blog.csdn.net/qq_41969879…




相关文章
|
1月前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
31 0
|
14天前
|
缓存 算法 搜索推荐
Java中的算法优化与复杂度分析
在Java开发中,理解和优化算法的时间复杂度和空间复杂度是提升程序性能的关键。通过合理选择数据结构、避免重复计算、应用分治法等策略,可以显著提高算法效率。在实际开发中,应该根据具体需求和场景,选择合适的优化方法,从而编写出高效、可靠的代码。
25 6
|
1月前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
2月前
|
监控 算法 Java
jvm-48-java 变更导致压测应用性能下降,如何分析定位原因?
【11月更文挑战第17天】当JVM相关变更导致压测应用性能下降时,可通过检查变更内容(如JVM参数、Java版本、代码变更)、收集性能监控数据(使用JVM监控工具、应用性能监控工具、系统资源监控)、分析垃圾回收情况(GC日志分析、内存泄漏检查)、分析线程和锁(线程状态分析、锁竞争分析)及分析代码执行路径(使用代码性能分析工具、代码审查)等步骤来定位和解决问题。
|
2月前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
174 6
|
2月前
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
48 2
|
2月前
|
设计模式 安全 Java
Java 多线程并发编程
Java多线程并发编程是指在Java程序中使用多个线程同时执行,以提高程序的运行效率和响应速度。通过合理管理和调度线程,可以充分利用多核处理器资源,实现高效的任务处理。本内容将介绍Java多线程的基础概念、实现方式及常见问题解决方法。
105 0
|
Java 编译器
java 学习 -深层拷贝 浅层拷贝 暑假第九天
 /*java 对象的克隆     实现Cloneable接口 但是这个接口中没有任何的 抽象方法 只是为了告诉 java 虚拟机这个对象可以被复制  然后我们在类中重写clone方法  这个方法从object定义 在子类中调用 super.
867 0
|
2天前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
32 17
|
12天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者