【问题标题】:How to read large files (a single continuous string) in Java?如何在 Java 中读取大文件(单个连续字符串)?
【发布时间】:2020-02-26 15:11:44
【问题描述】:

我正在尝试读取一个非常大的文件 (~2GB)。内容是带有句子的连续字符串(我想根据“。”来拆分它们)。无论我如何尝试,最终都会出现 Outofmemory 错误。

    BufferedReader in = new BufferedReader(new FileReader("a.txt"));
    String read = null;
    int i = 0;
    while((read = in.readLine())!=null) {
        String[] splitted = read.split("\\.");
        for (String part: splitted) {
            i+=1;
            users.add(new User(i,part));
            repository.saveAll(users);
        }
    }

还有,

inputStream = new FileInputStream(path);
    sc = new Scanner(inputStream, "UTF-8");
    while (sc.hasNextLine()) {
        String line = sc.nextLine();
        // System.out.println(line);
    }
    // note that Scanner suppresses exceptions
    if (sc.ioException() != null) {
        throw sc.ioException();
    }

文件内容(由随机单词组成,10个单词后加一个句号):

fmfbqi .xcdqnjqln kvjhw pexrbunnr cgvrqlr fpaczdegnb puqzjdbp gcfxne jawml aaiwwmo ugzoxn .opjc fmfbqi .xcdqnjqln kvjhw pexrbunnr cgvrqlr fpaczdegnb puqzjdbp gcfxne jawml aaiwwmo ugzoxn .opjc  (so on)

请帮忙!

【问题讨论】:

  • 赤裸裸的事实是:如果文件对您的记忆来说太大,并且您尝试一次全部读取,那么无论如何它都会失败。您是否出于某种原因实际上需要一次将所有内容存储在内存中?或者你可以逐块处理它(在你想要的任何垃圾中,也许是线条,也许在每个句号之后被切断,无论如何......)。因为如果可能的话,后者在资源需求上总是更容易。
  • 你试过java.nio.file.Files.lines()吗?将其流式传输...但是,如果您将拆分值存储在内存中的某个位置,则无论您如何读取文件,都可能会出现内存不足。
  • 如果该行中没有偶尔的换行符(即都是一行),那么Files.lines() 将遇到基本相同的问题,您需要逐块处理以其他方式分块。
  • 然后使用数据库或将其部分存储在文件中...
  • 如果没有换行符,则只有一行,因此只有一个行号。

标签: java filestream bufferedinputstream


【解决方案1】:

首先,正如 Joachim Sauer 所说,基于您的问题的 cmets:

如果没有换行符,则只有一行,因此只有一个行号。

所以你的用例充其量是有问题的。

让我们跳过这一点,假设可能有换行符 - 或者更好的是,假设您要拆分的 . 字符旨在作为换行符伪替换。

Scanner 在这里不是一个坏方法,尽管还有其他方法。由于您提供了Scanner,让我们继续,但您要确保将其包裹在BufferedReader 周围。您显然没有很多内存,BufferedReader 允许您读取由BufferedReader 缓冲的文件的“块”,同时利用Scanner 的功能对您来说完全模糊正在发生缓冲的调用者:

Scanner sc = new Scanner(new BufferedReader(new FileReader(new File("a.txt")), 10*1024));

这基本上是在让Scanner 像您期望的那样运行,但允许您一次缓冲 10MB,从而最大限度地减少内存占用。现在,你继续打电话

sc.useDelimiter("\\.");
for(int i = 0; sc.hasNext(); i++) {
    String psudeoLine = sc.next();
    //store line 'i' in your database for this psudeo-line
    //DO NOT store psudeoLine anywhere else - you don't have memory for it
}

由于您没有足够的内存,迭代(和重新迭代)的明确事项是在读取文件后不要将文件的任何部分存储在 JVM 的堆空间中。阅读它,根据需要使用它,并允许将其标记为 JVM 垃圾回收。在您的情况下,您提到要将伪行存储在数据库中,因此您想读取伪行,将其存储在数据库中,然后丢弃它。

这里还有其他需要指出的事情,例如配置 JVM 参数,但我什至不愿提及它,因为将 JVM 内存设置为高也是一个坏主意 - 另一种蛮力方法。将 JVM 内存最大堆大小设置得更高没有任何问题,但是如果您仍在学习如何编写软件,那么学习内存管理会更好。当您进入专业发展阶段时,您以后遇到的麻烦就会减少。

另外,我提到了ScannerBufferedReader,因为您在问题中提到了这一点,但我认为检查java.nio.file.Path.lines(),正如 deHaar 指出的那样也是一个好主意。这基本上与我明确列出的代码做同样的事情,但需要注意的是,它仍然一次只能执行 1 行,而无法更改您“拆分”的内容。因此,如果您的文本文件中有 1 行,这仍然会给您带来问题,您仍然需要像扫描仪这样的东西来分割行。

【讨论】:

  • 非常感谢您的详细回答! :)
  • 关于缓冲的建议可以扩展。 BufferedReader 的目的是限制对底层“源”的调用次数(最终,对此处读取的文件的系统调用)。这是一种优化:读取字节块比读取单个字节更有效。但。您已经有一个 FileReader,它将字节转换为 char,并使用内部缓冲区来执行此操作。所以你正在缓冲一个缓冲区。此缓冲对 Readers 是私有的,因此 Scanner 不会从中获利。 (另外,扫描器有自己的缓冲区来积累它读取的数据)。所以这 10MB 可能有点“浪费”。
  • 嗯,有点。 Scanner 更多的是混淆令牌解析。如果需要,您可以不使用 Scanner,但您也可以完全不使用 Java,并使用 JNI 和 fopen 直接从文件中读取字节。我并不是说任何方法都是错误的——我只是建议在易于编码、速度和内存管理之间存在一些中间立场。消除 Scanner 是公平的,当然,但是您必须继续手动执行超出缓冲区范围的令牌管理。
  • @searchengine27 我说的是删除 BufferedReader,它只在这里服务,IMO 缓冲(BufferedReader)一个缓冲区(FileReader 中的 StreamDecoder),无论如何它仍然会被缓冲(由扫描仪) .对于几乎没有性能提升来说,这似乎是在浪费内存。我同意删除 Scanner 会比保留它会产生更多的工作并打开可能的实现错误。我并不是要暗示应该将其处理掉。
猜你喜欢
  • 2011-01-13
  • 1970-01-01
  • 2020-03-25
  • 2018-06-06
  • 2020-07-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-13
相关资源
最近更新 更多