参考原文:http://www.360doc.com/content/13/0502/23/7669533_282552666.shtml
JAVA类库中的NIO包相对于IO 包来说有一个新功能是内存映射文件,日常编程中并不是经常用到,但是在处理大文件时是比较理想的提高效率的手段。 在传统的文件IO操作中,我们都是调用操作系统提供的底层标准IO系统调用函数 read()、write() ,此时调用此函数的进程(在JAVA中即java进程)由当前的用户态切换到内核态,然后OS的内核代码负责将相应的文件数据读取到内核的IO缓冲区,然后再把数据从内核IO缓冲区拷贝到进程的私有地址空间中去,这样便完成了一次IO操作。为什么要多此一举搞一个内核IO缓冲区把原本只需一次拷贝数据的事情搞成需要2次数据拷贝呢? 我想学过操作系统或者计算机系统结构的人都知道,这么做是为了减少磁盘的IO操作,为了提高性能而考虑的,因为我们的程序访问一般都带有局部性,也就是所谓的局部性原理,在这里主要是指的空间局部性,即我们访问了文件的某一段数据,那么接下去很可能还会访问接下去的一段数据,由于磁盘IO操作的速度比直接访问内存慢了好几个数量级,所以OS根据局部性原理会在一次 read()系统调用过程中预读更多的文件数据缓存在内核IO缓冲区中,当继续访问的文件数据在缓冲区中时便直接拷贝数据到进程私有空间,避免了再次的低效率磁盘IO操作。在JAVA中当我们采用IO包下的文件操作流,例如:
FileInputStream fileInputStream = new FileInputStream("D:\\Warcraft III.zip"); int read = fileInputStream.read();JAVA虚拟机内部便会调用OS底层的 read()系统调用完成操作,如上所述,在第二次调用 in.read()的时候可能就是从内核缓冲区直接返回数据了(可能还有经过 native堆做一次中转,因为这些函数都被声明为 native,即本地平台相关,所以可能在C代码中有做一次中转,如 win32中是通过 C代码从OS读取数据,然后再传给JVM内存)。既然如此,JAVA的IO包中为啥还要提供一个 BufferedInputStream 类来作为缓冲区呢。关键在于四个字,"系统调用"!当读取OS内核缓冲区数据的时候,便发起了一次系统调用操作(通过native的C函数调用),而系统 调用的代价相对来说是比较高的,涉及到进程用户态和内核态的上下文切换等一系列操作,所以我们经常采用如下的包装:
FileInputStream fileInputStream = new FileInputStream("D:\\Warcraft III.zip"); BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream); int read = bufferedInputStream.read();
内存映射文件和之前说的标准IO操作最大的不同之处就在于它虽然最终也是要从磁盘读取数据,但是它并不需要将数据读取到OS内核缓冲区,而是直接将进程的用户私有地址空间中的一 部分区域与文件对象建立起映射关系,就好像直接从内存中读、写文件一样,速度当然快了。为了说清楚这个,我们以 Linux操作系统为例子,看下图:
此图为 Linux 2.X 中的进程虚拟存储器,即进程的虚拟地址空间,如果你的机子是 32 位,那么就有 2^32 = 4G的虚拟地址空间,我们可以看到图中有一块区域: “Memory mapped region for shared libraries” ,这段区域就是在内存映射文件的时候将某一段的虚拟地址和文件对象的某一部分建立起映射关系,此时并没有拷贝数据到内存中去,而是当进程代码第一次引用这 段代码内的虚拟地址时,触发了缺页异常,这时候OS根据映射关系直接将文件的相关部分数据拷贝到进程的用户私有空间中去,当有操作第N页数据的时候重复这 样的OS页面调度程序操作。注意啦,原来内存映射文件的效率比标准IO高的重要原因就是因为少了把数据拷贝到OS内核缓冲区这一步(可能还少了 native堆中转这一步)。
java中提供了3种内存映射模式,即:只读(readonly)、读写(read_write)、专用(private) ,对于 只读模式来说,如果程序试图进行写操作,则会抛出ReadOnlyBufferException异 常;第二种的读写模式表明了通过内存映射文件的方式写或修改文件内容的话是会立刻反映到磁盘文件中去的,别的进程如果共享了同一个映射文件,那么也会立即 看到变化!而不是像标准IO那样每个进程有各自的内核缓冲区,比如JAVA代码中,没有执行 IO输出流的 flush() 或者 close() 操作,那么对文件的修改不会更新到磁盘去,除非进程运行结束;最后一种专用模式采用的是OS的“写时拷贝”原则,即在没有发生写操作的情况下,多个进程之 间都是共享文件的同一块物理内存(进程各自的虚拟地址指向同一片物理地址),一旦某个进程进行写操作,那么将会把受影响的文件数据单独拷贝一份到进程的私 有缓冲区中,不会反映到物理文件中去。
/** * Mode for a read-only mapping. */ public static final MapMode READ_ONLY = new MapMode("READ_ONLY"); /** * Mode for a read/write mapping. */ public static final MapMode READ_WRITE = new MapMode("READ_WRITE"); /** * Mode for a private (copy-on-write) mapping. */ public static final MapMode PRIVATE = new MapMode("PRIVATE");
在JAVA NIO中可以很容易的创建一块内存映射区域,代码如下:
File file = new File("E:\\download\\office2007pro.chs.ISO"); FileInputStream in = new FileInputStream(file); FileChannel channel = in.getChannel(); MappedByteBuffer buff = channel.map(FileChannel.MapMode.READ_ONLY, 0,channel.size());
这里创建了一个只读模式的内存映射文件区域,接下来我就来测试下与普通NIO中的通道操作相比性能上的优势,先看如下代码:
-
package org.apache.commons.csv; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; public class IOTest { static final int BUFFER_SIZE = 1024; public static void main(String[] args) throws Exception { String pathname = "D:\\Warcraft III.zip"; readByMappedByteBuffer(pathname); readByByteBuffer(pathname); } private static void readByMappedByteBuffer(String pathname) throws IOException { long begin = System.currentTimeMillis(); File file = new File(pathname); FileInputStream in = new FileInputStream(file); FileChannel channel = in.getChannel(); System.out.println(channel.size()); MappedByteBuffer buff = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()); byte[] b = new byte[1024]; int len = (int) file.length(); for (int offset = 0; offset < len; offset += 1024) { if (len - offset > BUFFER_SIZE) { buff.get(b); } else { buff.get(new byte[len - offset]); } } long end = System.currentTimeMillis(); System.out.println("time is:" + (end - begin)); } private static void readByByteBuffer(String pathname) throws IOException { long begin = System.currentTimeMillis(); File file = new File(pathname); FileInputStream in = new FileInputStream(file); FileChannel channel = in.getChannel(); ByteBuffer buff = ByteBuffer.allocate(1024); while (channel.read(buff) != -1) { buff.flip(); buff.clear(); } long end = System.currentTimeMillis(); System.out.println("time is:" + (end - begin)); } }
输出为 618毫秒,即通过内存映射文件的方式读取1.86GB大小的文件只需要618毫秒,而改为普通NIO的通道操作看下输出为 5683毫秒,几乎是 9 倍的差距。文件越大,差距便越大。所以内存映射文件特别适合于对大文件的操作,JAVA中的限制是最大不得超过 Integer.MAX_VALUE,即2G左右,不过我们可以通过分次映射文件(channel.map)的不同部分来达到操作整个文件的目的。 按照jdk文档的官方说法,内存映射文件属于JVM中的直接缓冲区,还可以通过 ByteBuffer.allocateDirect() ,即DirectMemory的方式来创建直接缓冲区。他们相比基础的 IO操作来说就是少了中间缓冲区的数据拷贝开销。同时他们属于JVM堆外内存,不受JVM堆内存大小的限制。 其中 DirectMemory 默认的大小是等同于JVM最大堆减去一个SURVIVOR区域的值,理论上说受限于进程的虚拟地址空间大小,比如 32位的windows上,每个进程有4G的虚拟空间除去 2G为OS内核保留外,再减去 JVM堆的最大值,剩余的才是DirectMemory大小。再看一个实例用内存映射文件和directBuffer进行文件copy: -
import sun.nio.ch.DirectBuffer; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; /** * Created by Administrator on 2015/4/15. */ public class Test { public static final long COPY_FILE_ZIE = 1024 * 1024 * 1000; public static void main(String[] args) throws IOException { long start = System.currentTimeMillis(); copyFileByMappedByteBuffer("D:\\Warcraft III.zip", "E:\\copy_of_Warcraft III.zip"); System.out.println("copyFileByMappedByteBuffer cost " + (System.currentTimeMillis() - start) + "ms"); start = System.currentTimeMillis(); copyFileByByteBuffer("D:\\Warcraft III.zip", "E:\\copy_of_Warcraft III.zip", ByteBuffer.allocateDirect(1024 * 1024 * 100), true); System.out.println("copyFileByDirectByteBuffer cost " + (System.currentTimeMillis() - start) + "ms"); } private static void copyFileByMappedByteBuffer(String srcFileName, String destFileName) throws IOException { FileChannel inFileChannel = null; FileChannel outFileChannel = null; MappedByteBuffer mappedByteBuffer = null; try { inFileChannel = new RandomAccessFile(srcFileName, "r").getChannel(); outFileChannel = new RandomAccessFile(destFileName, "rw").getChannel(); long fileSize = inFileChannel.size(); long position = 0; while (position < fileSize) { long copyFileSize = Math.min(fileSize - position, COPY_FILE_ZIE); mappedByteBuffer = outFileChannel.map(FileChannel.MapMode.READ_WRITE, position, copyFileSize); inFileChannel.read(mappedByteBuffer); position += mappedByteBuffer.position(); } } finally { closeStream(inFileChannel, outFileChannel); if (mappedByteBuffer != null) { ((DirectBuffer) mappedByteBuffer).cleaner().clean(); } } } private static void copyFileByByteBuffer(String srcFileName, String destFileName, ByteBuffer byteBuffer, boolean cleanDirectBuffer) throws IOException { FileChannel inFileChannel = null; FileChannel outFileChannel = null; try { inFileChannel = new RandomAccessFile(srcFileName, "r").getChannel(); outFileChannel = new RandomAccessFile(destFileName, "rw").getChannel(); int size = inFileChannel.read(byteBuffer); while (size > 0) { byteBuffer.flip(); outFileChannel.write(byteBuffer); byteBuffer.clear(); size = inFileChannel.read(byteBuffer); } } finally { closeStream(inFileChannel, outFileChannel); if (byteBuffer.isDirect() && cleanDirectBuffer) { ((DirectBuffer) byteBuffer).cleaner().clean(); } } } private static void closeStream(FileChannel inFileChannel, FileChannel outFileChannel) throws IOException { if (inFileChannel != null) { inFileChannel.close(); } if (outFileChannel != null) { outFileChannel.close(); } } }
-
- 运行结果:
copyFileByMappedByteBuffer cost 1825ms
copyFileByDirectByteBuffer cost 34188ms
- 通过设置 JVM参数 -Xmx64M -Xmn10M,即JVM最大堆为64M,然后执行以下程序可以证明DirectMemory不受JVM堆大小控制:
-
ByteBuffer.allocateDirect(1024 * 1024 * 64); // 64MB
我们设置了JVM堆 最大值为64M限制,其中年轻带大小为10M,然后在 直接内存上分配了64MB空间,程序执行后直接报错:Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory。然后修改为分配63M(由于年轻代为10M,一个SURVIVOR占用1M)的空间成功。然后我修改配置: -Xmx64M -XX:MaxDirectMemorySize=200M,分配一个200M空间成功。因此得出结论: 直接内存DirectMemory的大小默认为 -Xmx 的JVM堆的最大值减去一个survivor区域值,但是并不受其限制,而是由JVM参数 MaxDirectMemorySize单独控制。接下来我们来证明直接内存不是分配在JVM堆中。我们先执行以下程序,并设置 JVM参数 -Xmx64M -Xmn10M -XX:MaxDirectMemorySize=200M -XX:+PrintGCDetails
- 运行结果:
-
ByteBuffer.allocateDirect(1024 * 1024 * 200); ByteBuffer.allocateDirect(1024 * 1024 * 1);
- 输出结果如下:
- [GC (System.gc()) 1803K->880K(64512K), 0.0014972 secs]
- [Full GC (System.gc()) 880K->765K(64512K), 0.0070394 secs]
此时看到JVM发生了一次FULL GC。后面跟着System.gc(),说明这次FULL GC是由System.gc()发起的。查看DirectBuffer源码在其构造函数中间接调用了java.nio.Bits类的reserveMemory方法。
// These methods should be called whenever direct memory is allocated or // freed. They allow the user to control the amount of direct memory // which a process may access. All sizes are specified in bytes. static void reserveMemory(long size, int cap) { synchronized (Bits.class) { if (!memoryLimitSet && VM.isBooted()) { maxMemory = VM.maxDirectMemory(); memoryLimitSet = true; } // -XX:MaxDirectMemorySize limits the total capacity rather than the // actual memory usage, which will differ when buffers are page // aligned. if (cap <= maxMemory - totalCapacity) { reservedMemory += size; totalCapacity += cap; count++; return; } } System.gc(); try { Thread.sleep(100); } catch (InterruptedException x) { // Restore interrupt status Thread.currentThread().interrupt(); } synchronized (Bits.class) { if (totalCapacity + cap > maxMemory) throw new OutOfMemoryError("Direct buffer memory"); reservedMemory += size; totalCapacity += cap; count++; } }发现在cap <= maxMemory - totalCapacity时,也就是分配的直接内存大小朝富哦最大直接内存的时候,会进行一次System.gc()调用。
相关推荐
在Java NIO中,内存映射文件(MappedByteBuffer)是一个重要的特性,它允许将文件直接映射到内存中,以便于快速访问和修改文件内容。这一特性不仅提高了读写效率,而且还能用于进程间通信(IPC)。 内存映射文件的...
在Java NIO中,处理超大数据文件的关键在于合理利用内存映射文件(Memory-Mapped Files)来提高读取效率。内存映射文件是一种将文件内容直接映射到内存中的技术,使得对文件的操作如同操作内存一样简单高效。下面...
通过`FileChannel.map()`方法,可以直接将文件映射到内存中,这样读写文件就像操作内存一样快速。但需要注意的是,内存映射文件可能会消耗大量内存,因此对于大文件操作要谨慎使用。 5. **选择器(Selectors)** ...
6. **内存映射文件(Memory-Mapped Files)**: NIO允许将文件直接映射到内存,使得读写文件就像操作普通缓冲区一样快速。通过MappedByteBuffer类,可以直接在内存中对文件进行操作,提高大文件处理的性能。 7. **...
4. **FileChannel**:用于文件的读写,可以实现大文件的高效传输,支持内存映射(Mmap)技术,能直接将文件映射到内存中。 5. **SocketChannel**:用于网络通信,可以建立TCP连接,进行非阻塞的读写。在`NIOServer....
Java NIO提供了FileChannel用于文件操作,支持随机访问,可以进行文件的读写、映射到内存、文件大小的改变等操作。与标准IO相比,NIO的文件操作更高效且功能更强大。 5. **管道(Pipe)** 管道是两个线程间通信的...
3. **内存映射文件(Memory-Mapped Files)**:Java NIO提供了一个特殊的功能,即内存映射文件,它可以将文件直接映射到内存中,使得文件操作如同访问内存一样快速。对于大文件处理,内存映射文件是一种高效的策略,...
Java NIO(New IO)是Java 1.4版本引入的一个新模块,它提供了一种不同于标准Java IO API的处理I/O操作的方式。NIO的主要特点是面向缓冲区,非阻塞I/O,以及选择器,这些特性使得NIO在处理大量并发连接时表现出更高...
Java NIO(New Input/Output)是Java标准库提供的一种I/O模型,它与传统的 Blocking I/O(IO)相比,提供了更加高效的数据传输方式。在Java NIO中,"新"主要体现在非阻塞和多路复用这两个特性上,这使得NIO更适合于...
java NIO和java并发编程的书籍java NIO和java并发编程的书籍java NIO和java并发编程的书籍java NIO和java并发编程的书籍java NIO和java并发编程的书籍java NIO和java并发编程的书籍java NIO和java并发编程的书籍java...
在Java NIO中,零拷贝主要体现在通过DMA(Direct Memory Access,直接内存访问)和操作系统内核层面的优化来实现。 1. **NIO基础** - **通道(Channels)**:通道类似于流,但它是双向的,可以读也可以写。常见的...
4. **文件通道(File Channel)**:文件通道是直接与文件系统交互的通道,支持映射文件到内存(Memory-Mapped File)进行高速读写。 5. **网络通道(Network Channels)**:如SocketChannel和ServerSocketChannel,...
4. **内存映射文件**:`MappedByteBuffer`允许将文件直接映射到内存,提供了极高的读写速度。 然而,尽管Java NIO提供了很多优势,但在实际使用中,开发者还需要注意一些潜在的问题,如内存管理(缓冲区过大可能...
Java NIO系列教程(一) Java NIO 概述 Java NIO系列教程(二) Channel Java NIO系列教程(三) Buffer Java NIO系列教程(四) Scatter/Gather Java NIO系列教程(五) 通道...Java NIO系列教程(十二) Java NIO与IO
PFT(可能指的是文件传输或处理服务)在本场景中可能是通过JAVA NIO实现的一个功能模块,用于提供文件的上传、下载和列表展示服务。 文件服务是任何应用程序中不可或缺的部分,尤其是在Web环境中。在JAVA NIO中,...
通过FileChannel的map()方法,可以将文件映射到内存,从而提高大文件处理的性能。例如,以下代码展示了如何使用内存映射文件读取10MB的数据: ```java FileChannel fc = FileChannel.open(Paths.get("./largeFile....
使用MappedByteBuffer,NIO可以将文件映射到内存,使得文件操作如同操作内存一样快速,特别适合大数据处理。 在实际应用中,Java NIO通常用于高性能的服务器编程,例如在开发聊天服务器、Web服务器或游戏服务器时...
### Java NIO 原理与使用详解 #### 一、Java NIO 概述 在深入了解 Java NIO 的工作原理及其使用之前,我们首先来了解一下什么是 Java NIO(New I/O)。Java NIO 是 Java SE 1.4 版本引入的一个全新的 I/O API,...