前言
java通常采用BufferedReader,BufferedInputStream等带缓冲的IO类处理文件读写,不过java nio中引入了一种基于MappedByteBuffer操作大文件的方式,其读写性能极高。MappedByteBuffer引入了内存映射文件的方法,该方案是建立的操作系统的内存管理机制上的。
操作系统的内存管理机制
操作系统的内存分为:物理内存与进程虚拟地址空间(即逻辑地址空间),物理地址大家都知道,就是真实的物理,那什么是进程虚拟地址空间?
原来当每次创建一个进程的时候,操作系统都会为该进程分配一块虚拟地址空间,如果是32位的操作系统就是4GB大小,之所以是4GB,是因为在32位的操作系统中,一个指针长度是4字节,而4字节指针的寻址能力是从0x00000000~0xFFFFFFFF,最大值0xFFFFFFFF表示的即为4GB大小的容量。这4GB的虚拟地址空间分为2GB用户空间,2GB内核空间,进程应用程序只能访问自己对应的那2GB用户空间,而2GB的内核空间数据被所有应用程序共享,但是应用程序是不能直接访问。这两种地址空间的产生是为了隔离应用程序的数据,防止被除自己以为的其他应用程序恶意篡改。
此外,为了提高内存使用效率产生了分页机制,将物理内存和进程虚拟地址空间进行分页,页的大小由CPU决定,并且对与这两种地址空间产生的页的大小是相同的,例如,如果按照每页4KB的大小,4GB虚拟地址空间共可以分成1048576个页,512M的物理内存可以分为131072个页。显然虚拟空间的页数要比物理空间的页数多得多,在程序运行时,用到哪些页的数据就加载哪些页的数据到内存进行分配内存,并建立虚拟地址空间中的页和刚分配的物理内存页间的映射,没用到的页暂时保留在硬盘上。
一个完整的可执行应用程序的装载过程如下:一个可执行文件其实就是一些编译好的数据和指令的集合,它也会被分成很多页,为其分配虚拟地址空间的过程中会创建将来要进行内存映射的数据结构,这种数据结构就是页目和页表,当创建完这种数据结构之后,将把应用程序的数据一一映射到虚拟地址空间相应的页中,这时并没有真正将数据加载到内存,当CPU访问程序中用到的某一个虚拟地址,发现该地址并没有相关联的物理地址时,CPU会认为这是个页错误(Page Fault),从而知道操作系统还未给该虚拟页分配内存,CPU会将控制权交还给操作系统,操作系统在物理内存中为其分配页面,然后再将这个物理页面与虚拟空间中的虚拟页面映射起来,从而程序得以继续执行,这种页的加载有时候也被叫做缺页中断。值得注意的,当物理内存不够使用时,操作系统可以找到最少使用的页,将其失效,并将其回写到硬盘,修改映射关系,留出空余空间。
MappedByteBuffer原理
从继承结构上看,MappedByteBuffer继承自ByteBuffer,FileChannel提供了map方法把文件映射到进程虚拟地址空间,通常情况可以映射整个文件,如果文件比较大,可以进行分段映射。
FileChannel的Map方法的MapMode参数指定了内存映像文件访问的方式,共三种:
- MapMode.READ_ONLY:只读,试图修改得到的缓冲区将导致抛出异常。
- MapMode.READ_WRITE:读/写,对得到的缓冲区的更改最终将写入文件;但该更改对映射到同一文件的其他程序不一定是可见的。
- MapMode.PRIVATE:私用,可读可写,但是修改的内容不会写入文件,只是buffer自身的改变,这种能力称之为”copy on write”。
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完成文件的映射工作,并返回一个进程虚拟地址addr,第一次文件映射导致OOM,则手动触发垃圾回收,休眠100ms后再次尝试映射,如果失败,则抛出异常。
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; }}); }
最后返回的MappedByteBuffer实例是DirectByteBuffer类型,其实现了对内存的直接操作。
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); }
可以看出,MappedByteBuffer是通过map0方法返回的进程虚拟地址和偏移量进行操作文件,因为map0方法对数据和进程虚拟地址空间进行了映射,通过缺页中断机制可以进行文件的分段加载,代码中使用了unsafe.getByte方法,可见数据是直接分配的物理内存,而不是JVM的内存空间。
性能浅析:
- read()是系统调用,首先将文件从硬盘拷贝到内核空间的一个缓冲区,再将这些数据拷贝到用户空间,实际上进行了两次数据拷贝;
- map()也是系统调用,但没有进行数据拷贝,当缺页中断发生时,直接将文件从硬盘拷贝到用户空间,只进行了一次数据拷贝。
MappedByteBuffer的缺陷
使用MappedByteBuffer内存占用、文件关闭不确定,被其打开的文件只有在垃圾回收的才会被关闭,而且这个时间点是不确定的。
网络流传的解决方案如下:
AccessController.doPrivileged(new PrivilegedAction() { public Object run() { try { Method getCleanerMethod = buffer.getClass().getMethod("cleaner", new Class[0]); getCleanerMethod.setAccessible(true); sun.misc.Cleaner cleaner = (sun.misc.Cleaner) getCleanerMethod.invoke(byteBuffer, new Object[0]); cleaner.clean(); } catch (Exception e) { e.printStackTrace(); } return null; } });
话外语:
在利用FileChannel进行map映射内存文件的时候,一个文件可以被多个应用程序进行映射,事实上,这也是一种对于超大型文件在不同进程间数据共享的一种方式。
MappedByteBuffer的实例
public void testMappedByte() throws FileNotFoundException, IOException { long start = System.currentTimeMillis(); File file = new File("E:\\工作目录\\新项目\\凤舞一期\\backup\\new_show_style_json.txt"); long fileLength = file.length(); final int BUFFER_SIZE = 0x500000;// 5M MappedByteBuffer inputBuffer = new RandomAccessFile(file, "rw").getChannel().map(FileChannel.MapMode.READ_WRITE, 0, fileLength); byte[] dst = new byte[BUFFER_SIZE]; int count = 0; for (int offset = 0; offset < fileLength; offset += BUFFER_SIZE) { if (fileLength - offset >= BUFFER_SIZE) { for (int i = 0; i < BUFFER_SIZE; i++) dst[i] = inputBuffer.get(offset + i); } else { for (int i = 0; i < fileLength - offset; i++) dst[i] = inputBuffer.get(offset + i); } String bs = new String(dst, "UTF-8");// 将buffer中的字节转成字符串 String[] ns = bs.split("\n"); for (String s : ns) { if (s.contains(" -1- ")) { count++; System.out.println(s.split("- 1 -")[0]); } } System.out.println(); // String s = IOUtils.toString(new ByteArrayInputStream(dst)); // System.out.println(s); } System.out.println("总处理条数:" + count); long end = System.currentTimeMillis(); System.out.println((end - start) / 1000);// 处理809M的文件,90000条数据,只用了6秒 }
相关推荐
Java NIO,全称为Non-Blocking Input/Output(非阻塞输入/输出),是Java标准库提供的一种替代传统的I/O模型的新技术。自Java 1.4版本引入NIO后,它为Java开发者提供了更高效的数据传输方式,尤其是在处理大量并发...
### Java NIO 处理超大数据文件的知识点详解 #### 一、Java NIO简介 Java NIO(New IO)是Java平台上的新输入/输出流API,它提供了与传统IO(即Java IO)不同的数据处理方式。NIO在Java 1.4版本引入,并在后续版本...
4. **文件系统(File Systems)**:NIO提供FileChannel和FileLock用于处理文件系统操作,支持随机访问、映射到内存(MappedByteBuffer)等功能。 三、Java NIO的工作流程 1. **打开通道**:首先,我们需要创建一个或多...
- Java NIO提供了一组文件系统操作API,例如FileChannel用于读写文件,MappedByteBuffer实现了内存映射文件,可以直接通过内存访问文件内容,提高了读写速度。 4. **缓冲区的分类** - **ByteBuffer**:用于处理...
使用MappedByteBuffer,NIO可以将文件映射到内存,使得文件操作如同操作内存一样快速,特别适合大数据处理。 在实际应用中,Java NIO通常用于高性能的服务器编程,例如在开发聊天服务器、Web服务器或游戏服务器时...
Java NIO中提供了多种类型的缓冲区,如ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer、MappedByteBuffer等,每种类型的缓冲区都有其特定的用途和应用场景。 通道...
5. **文件系统操作**:NIO提供FileChannel,可以高效地进行文件读写操作,包括映射文件到内存(MappedByteBuffer)。 6. **管道(Pipes)**:管道是两个线程之间进行单向数据传输的通道,用于线程间通信。 7. **...
4. **文件系统操作**:Java NIO提供了更灵活的文件操作,如文件映射(MappedByteBuffer)。 四、Java NIO的应用场景 1. **网络编程**:Java NIO在服务器端开发中,特别是高并发的TCP连接处理,如聊天服务器、游戏...
Java NIO(New IO)是Java 1.4版本引入的一个新API,全称为Non-blocking Input/Output,它提供了一种不同于传统IO的编程模型,传统IO基于块I/O,而NIO则基于通道(Channel)和缓冲区(Buffer)进行数据传输。NIO的...
### Java NIO 系列教程知识点详解 #### Java NIO 概述 Java NIO (New IO) 是从 Java 1.4 开始提供的一种新的 I/O 处理方式,旨在改进传统 Java IO API 的性能并引入更高效的数据处理机制。Java NIO 主要包括三大...
4. **内存映射文件(MappedByteBuffer)**:Java NIO提供了一种高效访问大文件的方式,即内存映射文件。通过映射文件到内存,可以直接通过内存操作文件,减少了磁盘I/O的开销。 5. **非阻塞模式**:与传统的阻塞IO...
### Java NIO 原理与使用详解 #### 一、Java NIO 概述 在深入了解 Java NIO 的工作原理及其使用之前,我们首先来了解一下什么是 Java NIO(New I/O)。Java NIO 是 Java SE 1.4 版本引入的一个全新的 I/O API,...
Java NIO(New IO,也称为Non-Blocking IO)是一种基于通道(Channel)和缓冲区(Buffer)的I/O操作方法,用于替代标准Java IO API。Java NIO提供了与标准IO不同的I/O工作方式,它是面向缓冲区、基于通道的I/O操作,...
Java NIO,全称为New Input/Output,是Java在1.4版本引入的一个新特性,旨在提供一种更高效、更具选择性的I/O模型。相比于传统的IO(-blocking I/O)模型,NIO引入了非阻塞I/O和选择器,极大地提高了处理大量并发...
Java NIO(New IO)是Java 1.4版本引入的一个新模块,它提供了一种新的方式来处理I/O操作,相比传统的IO流,NIO提供了更高效、更灵活的数据读写方式。在这个主题中,我们将深入探讨Java NIO如何用于写文件,特别是在...
此外,Java NIO的内存映射文件(MappedByteBuffer),允许文件或文件的一部分被映射到内存中,这使得对文件的访问和修改可以像访问内存一样简单快捷。 以上是Java NIO的核心概念和组件的介绍。在实际的编程实践中,...
Java NIO(New IO)是Java平台从JDK 1.4版本开始引入的一个新的IO API,它提供了与标准的IO API不同的IO工作方式。NIO代表非阻塞IO,其设计目标是提供一种更高效、更灵活的IO操作方式,特别是在处理大量并发连接时,...
### Java NIO 教程知识点详解 #### 一、Java NIO 概述 Java NIO(New IO),从 Java 1.4 开始引入,是 Java 标准 IO API 的一个补充,提供了与标准 IO 不同的工作方式。Java NIO 的主要特性包括: 1. **基于通道...