关于大文件小内存的问题很常见,今天心血来潮想试试自己写一下这个代码。
1.生成 4G 的数字文本文件
通过计算每行的字节大小,累加到 4G 的话就停止写入
/**
* 生成一个4G的文件
*
* @param file
* @throws IOException
*/
public static void makeBigFile(File file) throws IOException {
FileOutputStream fos = new FileOutputStream(file);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(fos, "utf-8"));
// 随机数生成种子
Random rand = new Random(1);
long fileByteLength = 0;
// BIG_FILE_SIZE为4G
while (fileByteLength < BIG_FILE_SIZE) {
String randomIntStr = rand.nextInt(1000000000) + "";
bw.write(randomIntStr);
bw.newLine();
// 文件大小等于数字字符串的字符串字节大小加上换行符为两个字节
fileByteLength += randomIntStr.getBytes().length + 2;
}
bw.close();
}
2.将大文件分割为 256mb 的小文件在内存中进行排序
当超过 256mb 文件时,即更换另一个新的文件。(第一次写,条件没设置好,结果生成了一百多万个空文件,删除花了 N 久)
/**
* 将大文件拆分为不同的小文件
*
* @param bigFile 4G大文件
* @throws IOException
*/
public static void splitBigFileToSmallFile(File bigFile, String dirStr) throws IOException {
// 提前创建目录
File dir = new File(dirStr);
if (!dir.exists()) {
dir.mkdirs(); //创建目录
}
BufferedInputStream fis = new BufferedInputStream(new FileInputStream(bigFile));
// 用5M的缓冲读取文本文件
BufferedReader reader = new BufferedReader(new InputStreamReader(fis, "utf-8"), BUFFER_SIZE);
String line = null;
int fileNum = 1;
long smallFileByteLength = 0;
File smallFile = new File(dirStr + fileNum + ".txt");
FileOutputStream fos = new FileOutputStream(smallFile);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(fos, "utf-8"));
while ((line = reader.readLine()) != null) {
smallFileByteLength += getRowByteLength(line);
bw.write(line);
bw.newLine();
// 当文件超过固定大小的话,进行拆分
if (smallFileByteLength > FILE_LIMIT_SIZE) {
bw.flush();
// 文件数递增
fileNum++;
// 小文件的字节大小重置为0
smallFileByteLength = 0;
smallFile = new File(dirStr + fileNum + ".txt");
fos = new FileOutputStream(smallFile);
bw = new BufferedWriter(new OutputStreamWriter(fos, "utf-8"));
}
// 避免代码错误生成非常多小文件
if (fileNum > 20) {
break;
}
}
bw.flush();
bw.close();
}
3.对每个小文件进行内存排序,并重新写入到文件中
不能采用 ArrayList,因为当扩容的时候,旧数组和新的扩容数组会导致内存溢出
/**
* 对每个小文件内的数字进行排序
*
* @param file
* @throws IOException
*/
public static void sortEverySmallFile(File file) throws IOException {
// ArrayList涉及扩容 我们直接使用数组 否则数组扩容需要拷贝的时候 内存会溢出
BufferedInputStream countFis = new BufferedInputStream(new FileInputStream(file));
// 用5M的缓冲读取文本文件
BufferedReader countReader = new BufferedReader(new InputStreamReader(countFis, "utf-8"), BUFFER_SIZE);
// 统计行数,便于声明数组的长度
int rows = 0;
while (countReader.readLine() != null) {
rows++;
}
// 由于统计完行数的话,reader下标没法重置,所以我们重新载入文件给新的reader
BufferedInputStream fis = new BufferedInputStream(new FileInputStream(file));
// 用5M的缓冲读取文本文件
BufferedReader reader = new BufferedReader(new InputStreamReader(fis, "utf-8"), BUFFER_SIZE);
int[] numbers = new int[rows];
for (int i = 0; i < numbers.length; i++) {
numbers[i] = Integer.valueOf(reader.readLine());
}
System.out.println("开始排序");
Arrays.sort(numbers);
// 将排序好的数组写入到小文件中
writeSortToSmallFile(file, numbers);
}
4.合并已排序的小文件
我们可以把已排序的小文件看成是数组,借鉴合并两个有序数组的思路,我们这次采用合并 16 个有序数组的思路进行合并。
/**
* 将排序好的小文件进行归并排序
* 思路是借鉴 两个有序数组的合并
* 这次是合并16个有序数组
*
* @param smallFiles
* @throws IOException
*/
public static void mergeSmallFilesToBigFile(File[] smallFiles, String sortedBigFilePath) throws IOException {
// 小文件数组为空的话 直接返回
if (smallFiles == null || smallFiles.length == 0) {
return;
}
// 为每个小文件生成reader读取每行数字
BufferedReader[] smallFileReaders = new BufferedReader[smallFiles.length];
for (int i = 0; i < smallFiles.length; i++) {
BufferedInputStream fis = new BufferedInputStream(new FileInputStream(smallFiles[i]));
// 用固定的缓冲读取文本文件
BufferedReader smallFileReader = new BufferedReader(new InputStreamReader(fis, "utf-8"), BUFFER_SIZE);
smallFileReaders[i] = smallFileReader;
}
// 每一轮比对的最小数字
Integer[] minArrays = new Integer[smallFiles.length];
// 预填充
for (int i = 0; i < smallFileReaders.length; i++) {
String line = smallFileReaders[i].readLine();
if (line != null) {
minArrays[i] = Integer.valueOf(line);
}
}
// 排序好的大文件
FileOutputStream sortedFos = new FileOutputStream(sortedBigFilePath);
BufferedWriter sortedBw = new BufferedWriter(new OutputStreamWriter(sortedFos, "utf-8"), BUFFER_SIZE);
long writeNumber = 0;
// 依次比对每个小文件的最小数字,如果是最小数字的话,继续找下一行
while (!isAllNull(minArrays)) {
int minReaderIndex = 0;
int minNumber = Integer.MAX_VALUE;
for (int i = 0; i < minArrays.length; i++) {
if (minArrays[i] != null && minArrays[i] < minNumber) {
minNumber = minArrays[i];
minReaderIndex = i;
}
}
sortedBw.write(minNumber + "");
sortedBw.newLine();
// 被挑选到最小数字的Reader往下移一行
String line = smallFileReaders[minReaderIndex].readLine();
if (line != null) {
minArrays[minReaderIndex] = Integer.valueOf(line);
} else {
minArrays[minReaderIndex] = null;
}
writeNumber++;
if (writeNumber % 10000000 == 0) {
System.out.println("写入一千万数字");
}
}
// 强制刷出
sortedBw.flush();
sortedBw.close();
}
5.完整流程
public static void main(String[] args) throws IOException {
// 先生成一个4G的文件 内包含随机文字
File file = new File(DESKTOP_DIR + "bigFile.txt");
try {
makeBigFile(file);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("生成大文件成功!");
// 分割大文件变为小文件
splitBigFileToSmallFile(file, DESKTOP_SMALL_DIR);
System.out.println("分割大文件成功!");
// 对生成的小文件进行排序并写入,利用256mb内存进行排序
File smallFileDir = new File(DESKTOP_SMALL_DIR);
File[] smallFiles = smallFileDir.listFiles();
if (smallFiles != null && smallFiles.length > 0) {
for (File smallFile : smallFiles) {
sortEverySmallFile(smallFile);
System.out.println("完成" + smallFile.getName() + "的排序!");
}
}
String sortedBigFilePath = DESKTOP_DIR + "sortedBigFile.txt";
// 对已排好序的小文件进行归并
mergeSmallFilesToBigFile(smallFiles, sortedBigFilePath);
System.out.println("对已排序的小文件合并成功!");
// 由于文本工具一般没办法打开4g的文件,于是将排序好的文件重新分割成小文件,校验是否正确
splitBigFileToSmallFile(new File(sortedBigFilePath), DESKTOP_SORTED_SMALL_DIR);
System.out.println("重新分割已排序的大文件成功!");
}
因为 JVM 内存区域本身占了一些内存,于是我将内存调至-Xmx290M,如果要严格限制在 256mb 的话,可以将 FILE_LIMIT_SIZE 调至比 256mb 更低(将会大于 16 个文件)。完整代码详见
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于