1. java 读取大文件的困难

java 读取文件的一般操作是将文件数据全部读取到内存中,然后再对数据进行操作。例如

Path path = Paths.get("file path");
byte[] data = Files.readAllBytes(path);
  • 1
  • 2

这对于小文件是没有问题的,但是对于稍大一些的文件就会抛出异常

Exception in thread "main" java.lang.OutOfMemoryError: Required array size too large
at java.nio.file.Files.readAllBytes(Files.java:3156)
  • 1
  • 2

从错误定位看出,Files.readAllBytes 方法最大支持 Integer.MAX_VALUE - 8 大小的文件,也即最大2GB的文件。一旦超过了这个限度,java 原生的方法就不能直接使用了。

2. 分次读取大文件

既然不能直接全部读取大文件到内存中,那么就应该把文件分成多个子区域分多次读取。这就会有多种方法可以使用。

(1) 文件字节流

对文件建立 java.io.BufferedInputStream ,每次调用 read() 方法时会接连取出文件中长度为 arraySize 的数据到 array 中。这种方法可行但是效率不高。

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException; /**
* Created by zfh on 16-4-19.
*/
public class StreamFileReader {
private BufferedInputStream fileIn;
private long fileLength;
private int arraySize;
private byte[] array; public StreamFileReader(String fileName, int arraySize) throws IOException {
this.fileIn = new BufferedInputStream(new FileInputStream(fileName), arraySize);
this.fileLength = fileIn.available();
this.arraySize = arraySize;
} public int read() throws IOException {
byte[] tmpArray = new byte[arraySize];
int bytes = fileIn.read(tmpArray);// 暂存到字节数组中
if (bytes != -1) {
array = new byte[bytes];// 字节数组长度为已读取长度
System.arraycopy(tmpArray, 0, array, 0, bytes);// 复制已读取数据
return bytes;
}
return -1;
} public void close() throws IOException {
fileIn.close();
array = null;
} public byte[] getArray() {
return array;
} public long getFileLength() {
return fileLength;
} public static void main(String[] args) throws IOException {
StreamFileReader reader = new StreamFileReader("/home/zfh/movie.mkv", 65536);
long start = System.nanoTime();
while (reader.read() != -1) ;
long end = System.nanoTime();
reader.close();
System.out.println("StreamFileReader: " + (end - start));
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

(2) 文件通道

对文件建立 java.nio.channels.FileChannel ,每次调用 read() 方法时会先将文件数据读取到分配的长度为 arraySizejava.nio.ByteBuffer 中,再从中将已经读取到的文件数据转化到 array 中。这种利用了NIO中的通道的方法,比传统的字节流读取文件是要快一些。

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel; /**
* Created by zfh on 16-4-18.
*/
public class ChannelFileReader {
private FileInputStream fileIn;
private ByteBuffer byteBuf;
private long fileLength;
private int arraySize;
private byte[] array; public ChannelFileReader(String fileName, int arraySize) throws IOException {
this.fileIn = new FileInputStream(fileName);
this.fileLength = fileIn.getChannel().size();
this.arraySize = arraySize;
this.byteBuf = ByteBuffer.allocate(arraySize);
} public int read() throws IOException {
FileChannel fileChannel = fileIn.getChannel();
int bytes = fileChannel.read(byteBuf);// 读取到ByteBuffer中
if (bytes != -1) {
array = new byte[bytes];// 字节数组长度为已读取长度
byteBuf.flip();
byteBuf.get(array);// 从ByteBuffer中得到字节数组
byteBuf.clear();
return bytes;
}
return -1;
} public void close() throws IOException {
fileIn.close();
array = null;
} public byte[] getArray() {
return array;
} public long getFileLength() {
return fileLength;
} public static void main(String[] args) throws IOException {
ChannelFileReader reader = new ChannelFileReader("/home/zfh/movie.mkv", 65536);
long start = System.nanoTime();
while (reader.read() != -1) ;
long end = System.nanoTime();
reader.close();
System.out.println("ChannelFileReader: " + (end - start));
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57

(3) 内存文件映射

这种方法就是把文件的内容被映像到计算机虚拟内存的一块区域,从而可以直接操作内存当中的数据而无需每次都通过 I/O 去物理硬盘读取文件。这是由当前 java 态进入到操作系统内核态,由操作系统读取文件,再返回数据到当前 java 态的过程。这样就能大幅提高我们操作大文件的速度。

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel; /**
* Created by zfh on 16-4-19.
*/
public class MappedFileReader {
private FileInputStream fileIn;
private MappedByteBuffer mappedBuf;
private long fileLength;
private int arraySize;
private byte[] array; public MappedFileReader(String fileName, int arraySize) throws IOException {
this.fileIn = new FileInputStream(fileName);
FileChannel fileChannel = fileIn.getChannel();
this.fileLength = fileChannel.size();
this.mappedBuf = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileLength);
this.arraySize = arraySize;
} public int read() throws IOException {
int limit = mappedBuf.limit();
int position = mappedBuf.position();
if (position == limit) {
return -1;
}
if (limit - position > arraySize) {
array = new byte[arraySize];
mappedBuf.get(array);
return arraySize;
} else {// 最后一次读取数据
array = new byte[limit - position];
mappedBuf.get(array);
return limit - position;
}
} public void close() throws IOException {
fileIn.close();
array = null;
} public byte[] getArray() {
return array;
} public long getFileLength() {
return fileLength;
} public static void main(String[] args) throws IOException {
MappedFileReader reader = new MappedFileReader("/home/zfh/movie.mkv", 65536);
long start = System.nanoTime();
while (reader.read() != -1);
long end = System.nanoTime();
reader.close();
System.out.println("MappedFileReader: " + (end - start));
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

看似问题完美解决了,我们肯定会采用内存文件映射的方法去处理大文件。但是运行结果发现,这个方法仍然不能读取超过2GB的文件,明明 FileChannel.map() 方法传递的文件长度是 long 类型的,怎么和 Integer.MAX_VALUE 有关系?

Exception in thread "main" java.lang.IllegalArgumentException: Size exceeds Integer.MAX_VALUE
at sun.nio.ch.FileChannelImpl.map(FileChannelImpl.java:868)
  • 1
  • 2

再从错误定位可以看到

size - The size of the region to be mapped; must be non-negative and no greater than Integer.MAX_VALUE

这可以归结到一些历史原因,还有 int 类型在 java 中的深入程度,但是本质上由于 java.nio.MappedByteBuffer 是直接继承自 java.nio.ByteBuffer 的,而后者的索引变量是 int 类型的,所以前者也只能最大索引到 Integer.MAX_VALUE 的位置。这样的话我们是不是就没有办法了?当然不是,一个内存文件映射不够用,那么试一试用多个就可以了。

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel; /**
* Created by zfh on 16-4-19.
*/
public class MappedBiggerFileReader {
private MappedByteBuffer[] mappedBufArray;
private int count = 0;
private int number;
private FileInputStream fileIn;
private long fileLength;
private int arraySize;
private byte[] array; public MappedBiggerFileReader(String fileName, int arraySize) throws IOException {
this.fileIn = new FileInputStream(fileName);
FileChannel fileChannel = fileIn.getChannel();
this.fileLength = fileChannel.size();
this.number = (int) Math.ceil((double) fileLength / (double) Integer.MAX_VALUE);
this.mappedBufArray = new MappedByteBuffer[number];// 内存文件映射数组
long preLength = 0;
long regionSize = (long) Integer.MAX_VALUE;// 映射区域的大小
for (int i = 0; i < number; i++) {// 将文件的连续区域映射到内存文件映射数组中
if (fileLength - preLength < (long) Integer.MAX_VALUE) {
regionSize = fileLength - preLength;// 最后一片区域的大小
}
mappedBufArray[i] = fileChannel.map(FileChannel.MapMode.READ_ONLY, preLength, regionSize);
preLength += regionSize;// 下一片区域的开始
}
this.arraySize = arraySize;
} public int read() throws IOException {
if (count >= number) {
return -1;
}
int limit = mappedBufArray[count].limit();
int position = mappedBufArray[count].position();
if (limit - position > arraySize) {
array = new byte[arraySize];
mappedBufArray[count].get(array);
return arraySize;
} else {// 本内存文件映射最后一次读取数据
array = new byte[limit - position];
mappedBufArray[count].get(array);
if (count < number) {
count++;// 转换到下一个内存文件映射
}
return limit - position;
}
} public void close() throws IOException {
fileIn.close();
array = null;
} public byte[] getArray() {
return array;
} public long getFileLength() {
return fileLength;
} public static void main(String[] args) throws IOException {
MappedBiggerFileReader reader = new MappedBiggerFileReader("/home/zfh/movie.mkv", 65536);
long start = System.nanoTime();
while (reader.read() != -1) ;
long end = System.nanoTime();
reader.close();
System.out.println("MappedBiggerFileReader: " + (end - start));
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77

3. 运行结果比较

用上面三种方法读取1GB文件,运行结果如下

StreamFileReader:  11494900386
ChannelFileReader: 11329346316
MappedFileReader: 11169097480
  • 1
  • 2
  • 3

读取10GB文件,运行结果如下

StreamFileReader:       194579779394
ChannelFileReader: 190430242497
MappedBiggerFileReader: 186923035795
  • 1
  • 2
  • 3

java 分次读取大文件的三种方法的更多相关文章

  1. PHP读取大文件的几种方法介绍

    读取大文件一直是一个头痛的问题,我们像使用php开发读取小文件可以直接使用各种函数实现,但一到大文章就会发现常用的方法是无法正常使用或时间太长太卡了,下面我们就一起来看看关于php读取大文件问题解决办 ...

  2. .NET读取Excel文件的三种方法的区别

    ASP.NET读取Excel文件方法一:采用OleDB读取Excel文件: 把Excel文件当做一个数据源来进行数据的读取操作,实例如下: public DataSet ExcelToDS(strin ...

  3. 【转】flash air中读取本地文件的三种方法

    actionscript中读取本地文件操作有两种代码如下 1.使用File和FileStream两个类,FileStream负责读取数据的所以操作:(同步操作) var stream:FileStre ...

  4. PHP读取大文件的几种方法

    场景:PHP读取超大文件,例如1G的日志文件,我这里使用的是400M的access.log文件 1.使用file直接读取 <?php $starttime=microtime_float(); ...

  5. java使用java.util.Properties读取properties文件的九种方法

    直接上代码: package com.test.test; import java.io.BufferedInputStream; import java.io.FileInputStream; im ...

  6. PHP读取远程文件的三种方法

    file_get_contents <?php$url = http://www.xxx.com/;$contents = file_get_contents($url);//如果出现中文乱码使 ...

  7. java分享第十六天( java读取properties文件的几种方法&java配置文件持久化:static块的作用)

     java读取properties文件的几种方法一.项目中经常会需要读取配置文件(properties文件),因此读取方法总结如下: 1.通过java.util.Properties读取Propert ...

  8. matlab读取cvs文件的几种方法

    matlab读取CVS文件的几种方法: 1,实用csvread()函数   csvread()函数有三种使用方法: 1.M = csvread('filename')2.M = csvread('fi ...

  9. VC中加载LIB库文件的三种方法

    VC中加载LIB库文件的三种方法 在VC中加载LIB文件的三种方法如下: 方法1:LIB文件直接加入到工程文件列表中   在VC中打开File View一页,选中工程名,单击鼠标右键,然后选中&quo ...

随机推荐

  1. (转)[小工具] Linux下列格式化工具 - column

    当你看到Linux下命令输出的列歪歪扭扭时,是不是看着很不爽?column 命令就可以方便地解决这个问题. 比如: 我们一般就只用到上面这一个用法. column的其他用法如下: 选项 含义 -c 字 ...

  2. javac的命令(-Xbootclasspath、-classpath与-sourcepath等)

    当编译源文件时,编译器常常需要识别出类型的有关信息.对于源文件中使用.扩展或实现的每个类或接口,编译器都需要其类型信息.这包括在源文件中没有明确提及.但通过继承提供信息的类和接口. 例如,当扩展 ja ...

  3. django+mysql+html简单demo之 views+html

    #coding=utf-8 from __future__ import unicode_literals from django.shortcuts import render,render_to_ ...

  4. 发布 .Net Core WebAPI 应用程序到 Docker

    目录 1. 创建 .net core webapi 项目 2. 编译应用 3. 创建 Dockerfile 文件 4. 上传文件到服务器 5. 生成Docker Image 6. 在Docker Co ...

  5. [PY3]——时间处理——datetime | calendar

    Python3的日期/时间处理模块 datetime的格式化符号 格式化符号 表示 %y 两位数的年份表示(00-99) %Y 四位数的年份表示(000-9999) %m 月份(01-12) %d 日 ...

  6. 【PhotoShop】模糊图片清晰处理

    1.Ctrl+J复制出背景副本.对副本模式选择“亮度”. 2.选择“滤镜”菜单下的“锐化>USM锐化”命令,在设置窗口中适当调节一下锐化参数,根据你原图模糊的情况来调节,本图采用锐化数量为“15 ...

  7. td 不换行 隐藏显示多余的部分(转)

    转自:http://sha-tians.iteye.com/blog/1996162 table中td固定宽度后overflow:hidden不生效的问题 博客分类: html/css/js   前两 ...

  8. ExtJs定时消息提示框,类似于QQ右下角提示,ExtJs如何定时向后台发出两个请求并刷新数据实例

    原文出自:https://blog.csdn.net/seesun2012 思路: 1.加载页面,加载Ext.TaskManager.start()方法: 2.执行定时器方法: 3.获取地址向后台发送 ...

  9. SQL Server 导入excel时“该值违反了该列的完整性约束”错误

    SQL Server 导入excel时“该值违反了该列的完整性约束”错误 这个问题看似高大上,仔细分析了一下,ID列怎么会有重复呢? 原来是有很多空行呀!!! 所以导入excel时一定要注意空行的问题 ...

  10. WPF实现动画的几种方式及其小案例

    WPF实现动画的方式: 基于计时器的动画 建立一个定时器,然后根据其频率循环调用函数或者一个事件处理函数,在这个函数中可以手工更新目标属性,直到达到最终值,这时可以停止计时器. 案例: 效果图: XA ...