Java高效地分割文本文件的方法技巧

 更新时间:2025年05月13日 08:29:57   作者:提前退休的java猿  
这篇文章介绍了Java中零拷贝技术的原理和应用,通过比较传统方法和零拷贝方法的性能,展示了如何使用FileChannel的transferTo方法高效地分割大型文本文件,特别是在保持行完整性的同时,显著提高了处理速度,需要的朋友可以参考下

前言

之前听到零拷贝的技术,都感觉好高深好遥远呀🥸

都是看什么什么框架用了零拷贝技术,比如netty就使用零拷贝技术。

看到一篇文章让我对接零拷贝技术去魅了,原来我也可以再工作中去使用零拷贝技术,今天把这篇文章分享给大家

低效常用示例

当我们面临将文本文件分成最大大小块的时,我们可能会尝试编写如下代码:

    private static final long maxFileSizeBytes = 10 * 1024 * 1024; // 默认10MB


    public void split(Path inputFile, Path outputDir) throws IOException {
        if (!Files.exists(inputFile)) {
            throw new IOException("输入文件不存在: " + inputFile);
        }
        if (Files.size(inputFile) == 0) {
            throw new IOException("输入文件为空: " + inputFile);
        }

        Files.createDirectories(outputDir);

        try (BufferedReader reader = Files.newBufferedReader(inputFile)) {
            int fileIndex = 0;
            long currentSize = 0;
            BufferedWriter writer = null;
            try {
                writer = newWriter(outputDir, fileIndex++);

                String line;
                while ((line = reader.readLine()) != null) {
                byte[] lineBytes = (line + System.lineSeparator()).getBytes();
                if (currentSize + lineBytes.length > maxFileSizeBytes) {
                    if (writer != null) {
                        writer.close();
                    }
                    writer = newWriter(outputDir, fileIndex++);
                    currentSize = 0;
                }
                writer.write(line);
                writer.newLine();
                currentSize += lineBytes.length;
                }
            } finally {
                if (writer != null) {
                    writer.close();
                }
            }
        }
    }

    private BufferedWriter newWriter(Path dir, int index) throws IOException {
        Path filePath = dir.resolve("part_" + index + ".txt");
        return Files.newBufferedWriter(filePath);
    }

效率分析

此代码在技术上是可以的,但是将大文件拆分为多个块的效率非常低。

它执行许多堆分配 (行),导致创建和丢弃大量临时对象 (字符串、字节数组) 。
还有一个不太明显的问题,它将数据复制到多个缓冲区,并在用户和内核模式之间执行上下文切换。

具体如下:

BufferedReader: BufferedReader 的 BufferedReader 中:

  • 在底层 FileReaderInputStreamReader 上调用 read()
  • 数据从内核空间用户空间缓冲区复制。
  • 然后解析为 Java 字符串(堆分配)。

getBytes() : getBytes()

  • String 转换为新的 byte[] 更多的堆分配。

BufferedWriter: BufferedWriter 的 BufferedWriter 中:

  • 从用户空间获取 byte/char 数据。
  • 调用 write()这又涉及将用户空间复制到内核空间。
  • 最终刷新到磁盘。

因此,数据在内核和用户空间之间来回移动多次,并产生额外的堆改动。除了垃圾收集压力外,它还具有以下后果:

  • 内存带宽浪费在缓冲区之间进行复制。
  • 磁盘到磁盘传输的 CPU 利用率较高。
  • 操作系统本可直接处理批量拷贝(通过DMA或优化I/O),但Java代码通过引入用户空间逻辑拦截了这种高效性。

高效处理方案

那么,我们如何避免上述问题呢?

答案是尽可能使用 zero copy,即尽可能避免离开 kernel 空间。这可以通过使用 FileChannel 方法 long transferTo(long position, long count, WritableByteChannel target) 在 java 中完成。它直接是磁盘到磁盘的传输,还会利用作系统的一些 IO 优化。

有问题就是所描述的方法对字节块进行作,可能会破坏行的完整性。为了解决这个问题,我们需要一种策略来确保即使通过移动字节段处理文件时,行也保持完整

没有上述的问题就很容易,只需为每个块调用 transferTo,将position递增为 position = position + maxFileSize,直到无法传输更多数据。

为了保持行的完整性,我们需要确定每个字节块中最后一个完整行的结尾。为此,我们首先查找 chunk 的预期末尾,然后向后扫描以找到前面的换行符。这将为我们提供 chunk 的准确字节计数,确保包含最后的、不间断的行。这将是执行缓冲区分配和复制的代码的唯一部分,并且由于这些作应该最小,因此预计性能影响可以忽略不计。

private static final int LINE_ENDING_SEARCH_WINDOW = 8 * 1024;
​
private long maxSizePerFileInBytes;
private Path outputDirectory;
private Path tempDir;
​
private void split(Path fileToSplit) throws IOException {
    try (RandomAccessFile raf = new RandomAccessFile(fileToSplit.toFile(), "r");
            FileChannel inputChannel = raf.getChannel()) {
​
        long fileSize = raf.length();
        long position = 0;
        int fileCounter = 1;
​
        while (position < fileSize) {
            // Calculate end position (try to get close to max size)
            long targetEndPosition = Math.min(position + maxSizePerFileInBytes, fileSize);
​
            // If we're not at the end of the file, find the last line ending before max size
            long endPosition = targetEndPosition;
            if (endPosition < fileSize) {
                endPosition = findLastLineEndBeforePosition(raf, position, targetEndPosition);
            }
​
            long chunkSize = endPosition - position;
            var outputFilePath = tempDir.resolve("_part" + fileCounter);
            try (FileOutputStream fos = new FileOutputStream(outputFilePath.toFile());
                    FileChannel outputChannel = fos.getChannel()) {
                inputChannel.transferTo(position, chunkSize, outputChannel);
            }
​
            position = endPosition;
            fileCounter++;
        }
​
    }
}
​
private long findLastLineEndBeforePosition(RandomAccessFile raf, long startPosition, long maxPosition)
        throws IOException {
    long originalPosition = raf.getFilePointer();
​
    try {
        int bufferSize = LINE_ENDING_SEARCH_WINDOW;
        long chunkSize = maxPosition - startPosition;
​
        if (chunkSize < bufferSize) {
            bufferSize = (int) chunkSize;
        }
​
        byte[] buffer = new byte[bufferSize];
        long searchPos = maxPosition;
​
        while (searchPos > startPosition) {
            long distanceToStart = searchPos - startPosition;
            int bytesToRead = (int) Math.min(bufferSize, distanceToStart);
​
            long readStartPos = searchPos - bytesToRead;
            raf.seek(readStartPos);
​
            int bytesRead = raf.read(buffer, 0, bytesToRead);
            if (bytesRead <= 0)
                break;
​
            // Search backwards through the buffer for newline
            for (int i = bytesRead - 1; i >= 0; i--) {
                if (buffer[i] == '\n') {
                    return readStartPos + i + 1;
                }
            }
​
            searchPos -= bytesRead;
        }
​
        throw new IllegalArgumentException(
                "File " + fileToSplit + " cannot be split. No newline found within the limits.");
    } finally {
        raf.seek(originalPosition);
    }
}

findLastLineEndBeforePosition 方法具有某些限制。具体来说,它仅适用于类 Unix 系统 (\n),非常长的行可能会导致大量向后读取迭代,并且包含超过 maxSizePerFileInBytes 的行的文件无法拆分。但是,它非常适合拆分访问日志文件等场景,这些场景通常具有短行和大量条目。

性能分析

理论上,我们zero copy拆分文件应该【常用方式】更快,现在是时候衡量它能有多快了。为此,我为这两个实现运行了一些基准测试,这些是结果。

Benchmark                                                    Mode  Cnt           Score      Error   Units
FileSplitterBenchmark.splitFile                              avgt   15        1179.429 ±   54.271   ms/op
FileSplitterBenchmark.splitFile:·gc.alloc.rate               avgt   15        1349.613 ±   60.903  MB/sec
FileSplitterBenchmark.splitFile:·gc.alloc.rate.norm          avgt   15  1694927403.481 ± 6060.581    B/op
FileSplitterBenchmark.splitFile:·gc.count                    avgt   15         718.000             counts
FileSplitterBenchmark.splitFile:·gc.time                     avgt   15         317.000                 ms
FileSplitterBenchmark.splitFileZeroCopy                      avgt   15          77.352 ±    1.339   ms/op
FileSplitterBenchmark.splitFileZeroCopy:·gc.alloc.rate       avgt   15          23.759 ±    0.465  MB/sec
FileSplitterBenchmark.splitFileZeroCopy:·gc.alloc.rate.norm  avgt   15     2555608.877 ± 8644.153    B/op
FileSplitterBenchmark.splitFileZeroCopy:·gc.count            avgt   15          10.000             counts
FileSplitterBenchmark.splitFileZeroCopy:·gc.time             avgt   15           5.000                 ms

以下是用于上述结果的基准测试代码和文件大小(200+MB)。

int maxSizePerFileInBytes = 1024 * 1024 // 1 MB chunks
​
public void setup() throws Exception {
    inputFile = Paths.get("/tmp/large_input.txt");
    outputDir = Paths.get("/tmp/split_output");
    // Create a large file for benchmarking if it doesn't exist
    if (!Files.exists(inputFile)) {
        try (BufferedWriter writer = Files.newBufferedWriter(inputFile)) {
            for (int i = 0; i < 10_000_000; i++) {
                writer.write("This is line number " + i);
                writer.newLine();
            }
        }
    }
}
​
public void splitFile() throws Exception {
    splitter.split(inputFile, outputDir);
}
​
public void splitFileZeroCopy() throws Exception {
    zeroCopySplitter.split(inputFile);
}

zeroCopy表现出相当大的加速,仅用了 77 毫秒,而对于这种特定情况,【常用方式】需要 1179 毫秒。在处理大量数据或许多文件时,这种性能优势可能至关重要。

结论

高效拆分大型文本文件需要系统级性能考虑,而不仅仅是逻辑。虽然基本方法突出了内存作过多的问题,但重新设计的解决方案利用零拷贝技术并保持行完整性,可以显著提高性能。

这证明了系统感知编程和理解 I/O 机制在创建更快、更节省资源的工具来处理大型文本数据(如日志或数据集)方面的影响。

以上就是Java高效地分割文本文件的方法技巧的详细内容,更多关于Java分割文本文件的资料请关注脚本之家其它相关文章!

相关文章

  • SpringBoot在 POM 中引入本地 JAR 包的方法

    SpringBoot在 POM 中引入本地 JAR 包的方法

    在开发 Spring Boot 应用程序时,您可能需要使用本地 JAR 包来添加自定义库或功能,本文将介绍在 Spring Boot 项目的 POM 文件中如何引入本地 JAR 包,感兴趣的朋友跟随小编一起看看吧
    2023-08-08
  • Java中使用Files类的copy()方法实现复制文件

    Java中使用Files类的copy()方法实现复制文件

    这篇文章主要介绍了Java中使用Files类的copy()方法实现复制文件,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-05-05
  • 深入探究Java线程的状态与生命周期

    深入探究Java线程的状态与生命周期

    在java中,任何对象都要有生命周期,线程也不例外,它也有自己的生命周期。线程的整个生命周期可以分为5个阶段,分别是新建状态、就绪状态、运行状态、阻塞状态和死亡状态
    2022-04-04
  • SpringBoot使用flyway初始化数据库

    SpringBoot使用flyway初始化数据库

    这篇文章主要介绍了SpringBoot如何使用flyway初始化数据库,帮助大家更好的理解和学习使用SpringBoot框架,感兴趣的朋友可以了解下
    2021-03-03
  • Maven resrouce下filtering的使用方法

    Maven resrouce下filtering的使用方法

    本文介绍了Maven的resource插件中的filtering功能,该功能用于在构建过程中将资源目录下的文件中的tokens进行参数替换,tokens的来源可以是pom文件中的properties属性或外部的.properties文件,通过这种方式,可以灵活地切换不同开发环境下的配置属性
    2024-11-11
  • Spring Cloud Ribbon 负载均衡使用策略示例详解

    Spring Cloud Ribbon 负载均衡使用策略示例详解

    Spring Cloud Ribbon 是基于Netflix Ribbon 实现的一套客户端负载均衡工具,Ribbon客户端组件提供了一系列的完善的配置,如超时,重试等,这篇文章主要介绍了Spring Cloud Ribbon 负载均衡使用策略示例详解,需要的朋友可以参考下
    2023-03-03
  • SpringBoot的启动过程源码详细分析

    SpringBoot的启动过程源码详细分析

    这篇文章主要介绍了SpringBoot的启动过程源码详细分析,SpringBoot启动的时候,会构造一个SpringApplication的实例,构造SpringApplication的时候会进行初始化的工作,需要的朋友可以参考下
    2023-11-11
  • java对数据库更新的操作方式及注意事项

    java对数据库更新的操作方式及注意事项

    这篇文章主要介绍了java对数据库更新的操作方式及注意事项,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-04-04
  • 一文详解JAVA中InputStreamReader流

    一文详解JAVA中InputStreamReader流

    本文主要介绍了一文详解JAVA中InputStreamReader流,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04
  • 解决反射调用方法时获取bean失败的问题

    解决反射调用方法时获取bean失败的问题

    文章描述了通过反射机制调用类方法时遇到的@Autowired注入失败和事务回滚失败的问题,原因是反射生成的对象未被SpringIOC容器管理,解决方案是通过applicationContext.getBean("className")方法获取Spring管理的bean来解决注入和事务问题
    2025-10-10

最新评论