【问题标题】:Java NIO MappedByteBuffer OutOfMemoryExceptionJava NIO MappedByteBuffer OutOfMemoryException
【发布时间】:2014-01-10 01:02:07
【问题描述】:

我真的遇到了麻烦:我想使用 FileChannels 和 MappedByteBuffers 读取数 GB 以上的巨大文件 - 我发现的所有文档都表明使用 FileChannel.map() 方法映射文件相当简单。 当然 2GB 是有限制的,因为所有 Buffer 方法都使用 int 来表示位置、限制和容量——但是低于该限制的系统隐含限制呢?

实际上,我遇到了很多关于OutOfMemoryExceptions 的问题!而且根本没有真正定义限制的文档! 那么 - 我如何才能将适合 int-limit 的文件安全地映射到一个或多个 MappedByteBuffers 而不会出现异常?

在尝试FileChannel.map() 之前,我可以询问系统我可以安全映射文件的哪个部分吗?如何? 为什么关于这个功能的文档这么少??

【问题讨论】:

    标签: java file-io nio bytebuffer filechannel


    【解决方案1】:

    您不会使用FileChannel API 一次写入整个文件。相反,您分部分发送文件。请参阅 Martin Thompson 比较 Java IO 技术性能的帖子中的示例代码:Java Sequential IO Performance

    此外,没有太多文档,因为您正在进行依赖于平台的调用。来自map()JavaDoc:

    内存映射文件的许多细节本质上是依赖的 在底层操作系统上,因此未指定。

    【讨论】:

    • 马丁汤普森很棒。请注意,对于 8GB 文件,FileChannel 实际上并没有快得多(!) - 但正如 Martin 所说的“您的里程可能会有所不同。”
    • 当您访问单个单词而不是流时,映射的 IO 要快得多。我已经看到运行that code。内存映射 io 为 50 MB/秒,直接使用 raf 则慢 300 倍。
    • 但是,RAF 对随机访问稍快一些。如果文件只有系统内存的 1/10,那么对于测试的数百万个请求,MM 接近 14 ns/request,而 raf 则慢 1000 倍,即 11 us/req。但是,如果文件大于主内存,则内存映射搜索需要 5 毫秒,而对于超过 20k 的随机访问,RAF 接近 4.8 毫秒。不同之处在于,无论如何,raf 总是访问文件,而 MMF 提供更强大的缓存。两者具有相同的访问时间,但 raf 只读取一个需要的单词,而 MMF 读取整个 4k 页面,因此在长文件中变得稍慢。
    • 抱歉,MMF 以 140 ns 的速度读取 1GB 文件中的随机 long,仅比 RAF 快 100 倍。有趣的是,当您向它请求 readLong 时,RAF 读取单个字节 8 次。
    【解决方案2】:

    我可以提供一些工作代码。这是否解决了你的问题很难说。这会在文件中寻找Hunter 识别的模式。

    原创研究见优秀文章Java tip: How to read files quickly(不是我的)。

    // 4k buffer size.
    static final int SIZE = 4 * 1024;
    static byte[] buffer = new byte[SIZE];
    
    // Fastest because a FileInputStream has an associated channel.
    private static void ScanDataFile(Hunter p, FileInputStream f) throws FileNotFoundException, IOException {
      // Use a mapped and buffered stream for best speed.
      // See: http://nadeausoftware.com/articles/2008/02/java_tip_how_read_files_quickly
      FileChannel ch = f.getChannel();
      long red = 0L;
      do {
        long read = Math.min(Integer.MAX_VALUE, ch.size() - red);
        MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, red, read);
        int nGet;
        while (mb.hasRemaining() && p.ok()) {
          nGet = Math.min(mb.remaining(), SIZE);
          mb.get(buffer, 0, nGet);
          for (int i = 0; i < nGet && p.ok(); i++) {
            p.check(buffer[i]);
          }
        }
        red += read;
      } while (red < ch.size() && p.ok());
      // Finish off.
      p.close();
      ch.close();
      f.close();
    }
    

    【讨论】:

      【解决方案3】:

      我使用的是List&lt;ByteBuffer&gt;,其中每个 ByteBuffer 映射到 16 MB 到 1 GB 的块中的文件。我使用 2 的幂来简化逻辑。我用它来映射高达 8 TB 的文件。

      内存映射文件的一个关键限制是您受到虚拟内存的限制。如果你有一个 32 位的 JVM,你将无法进行很多映射。

      我不会继续为文件创建新的内存映射,因为这些映射永远不会被清除。你可以创建很多,但在某些系统上似乎有大约 32K 的限制(无论它们有多小)

      我发现 MemoryMappedFiles 有用的主要原因是它们不需要被刷新(如果您可以假设操作系统不会死机)这允许您以低延迟的方式写入数据,而不必担心丢失太多如果应用程序因不得不 write() 或 flush() 而死机或性能过高,则数据。

      【讨论】:

      • 我有created a demo for your idea。谢谢,你的建议真的很有用。我只是不明白关闭缓冲区部分。如果文件在 Windows 中缓冲,您可能不需要关闭文件,并且它可以从缓存而不是硬盘为其他用户提供数据。但是,问题是除非您关闭它,否则其他用户无法打开该文件。从 scala 构建工具多次重新运行我的程序时,我得到了很多 cannot access the file
      • @ValentinTihomirov 您需要清理内存映射文件的缓冲区,否则文件将在 Windows 上保持锁定状态。
      • buf = null ; System.gc 你的意思是?
      【解决方案4】:

      文件越大,您就越不希望一次将其全部存储在内存中。设计一种方法来处理文件,一次是一个缓冲区,一次是一行,等等。

      MappedByteBuffers 尤其成问题,因为映射内存没有明确的释放,所以一次使用多个基本上肯定会失败。

      【讨论】:

      • 我完全走错了方向,想要映射整个文件,你是对的。我误解了将文件映射到内存的概念,认为一切都是虚拟的,操作系统会在需要时将页面加载到内存中......但是对于根本不起作用的千兆或 TB 文件。
      • @Zordid 它确实会根据需要加载页面,但它会一次映射所有内存,这需要交换空间、内存地址分配……所有宝贵的资源都没有“未定义的释放”时间.
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-12-06
      • 2012-12-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-11-25
      相关资源
      最近更新 更多