【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并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
1天前
|
安全 Java 程序员
Java中的多线程并发编程实践
【4月更文挑战第18天】在现代软件开发中,为了提高程序性能和响应速度,经常需要利用多线程技术来实现并发执行。本文将深入探讨Java语言中的多线程机制,包括线程的创建、启动、同步以及线程池的使用等关键技术点。我们将通过具体代码实例,分析多线程编程的优势与挑战,并提出一系列优化策略来确保多线程环境下的程序稳定性和性能。
|
2天前
|
缓存 分布式计算 监控
Java并发编程:深入理解线程池
【4月更文挑战第17天】在Java并发编程中,线程池是一种非常重要的技术,它可以有效地管理和控制线程的执行,提高系统的性能和稳定性。本文将深入探讨Java线程池的工作原理,使用方法以及在实际开发中的应用场景,帮助读者更好地理解和使用Java线程池。
|
3天前
|
缓存 监控 Java
Java并发编程:线程池与任务调度
【4月更文挑战第16天】Java并发编程中,线程池和任务调度是核心概念,能提升系统性能和响应速度。线程池通过重用线程减少创建销毁开销,如`ThreadPoolExecutor`和`ScheduledThreadPoolExecutor`。任务调度允许立即或延迟执行任务,具有灵活性。最佳实践包括合理配置线程池大小、避免过度使用线程、及时关闭线程池和处理异常。掌握这些能有效管理并发任务,避免性能瓶颈。
|
4天前
|
设计模式 运维 安全
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第15天】在Java开发中,多线程编程是提升应用程序性能和响应能力的关键手段。然而,它伴随着诸多挑战,尤其是在保证线程安全的同时如何避免性能瓶颈。本文将探讨Java并发编程的核心概念,包括同步机制、锁优化、线程池使用以及并发集合等,旨在为开发者提供实用的线程安全策略和性能优化技巧。通过实例分析和最佳实践的分享,我们的目标是帮助读者构建既高效又可靠的多线程应用。
|
6天前
|
Java 编译器
Java并发编程中的锁优化策略
【4月更文挑战第13天】 在Java并发编程中,锁是一种常见的同步机制,用于保证多个线程之间的数据一致性。然而,不当的锁使用可能导致性能下降,甚至死锁。本文将探讨Java并发编程中的锁优化策略,包括锁粗化、锁消除、锁降级等方法,以提高程序的执行效率。
12 4
|
7天前
|
Java 调度 开发者
Java 21时代的标志:虚拟线程带来的并发编程新境界
Java 21时代的标志:虚拟线程带来的并发编程新境界
15 0
|
存储 SQL 关系型数据库
最新精心整理Java面试题,实现原理分析
最新精心整理Java面试题,实现原理分析
|
10天前
|
存储 Java 数据库连接
java多线程之线程通信
java多线程之线程通信
|
10天前
|
算法 Java 开发者
Java中的多线程编程:概念、实现与性能优化
【4月更文挑战第9天】在Java编程中,多线程是一种强大的工具,它允许开发者创建并发执行的程序,提高系统的响应性和吞吐量。本文将深入探讨Java多线程的核心概念,包括线程的生命周期、线程同步机制以及线程池的使用。接着,我们将展示如何通过继承Thread类和实现Runnable接口来创建线程,并讨论各自的优缺点。此外,文章还将介绍高级主题,如死锁的预防、避免和检测,以及如何使用并发集合和原子变量来提高多线程程序的性能和安全性。最后,我们将提供一些实用的性能优化技巧,帮助开发者编写出更高效、更稳定的多线程应用程序。