截断内存映射文件

13

我正在使用内存映射IO来处理索引文件,但问题是如果该文件大部分为空,则无法调整文件大小。

之前某处:

MappedByteBuffer map = raf.getChannel().map(MapMode.READ_WRITE, 0, 1 << 30);
raf.close();
// use map
map.force();
map = null;

调整大小:

for (int c = 0; c < 100; c++) {
    RandomAccessFile raf = new RandomAccessFile(indexFile, "rw");
    try {
        raf.setLength(newLen);
        if (c > 0) LOG.warn("used " + c + " iterations to close mapped byte buffer");
        return;
    } catch (Exception e) {
        System.gc();
        Thread.sleep(10);
        System.runFinalization();
        Thread.sleep(10);
    } finally {
        raf.close();
    }
}

在使用Windows或Linux 32位时,我经常遇到取消映射问题,但在64位Linux生产环境中,一切似乎都没有警告地工作,但文件仍保留原始大小。

有人能解释一下为什么会这样发生和/或如何解决这个问题吗?


我担心问题可能与NFS、缓存或时间有关,因为它似乎在没有实际干预的情况下得到了解决(只是添加了日志和等待,现在它可以工作了)。即使是那些在截断后变得巨大且自那时以来未被触摸的文件现在也具有正确的大小。也许在截断后记录新文件大小会更新一些nfs缓存。 - rurouni
讨论的问题类似于“如何取消映射文件”,特别是请参见“bug#4724038”。 - dma_k
2个回答

8
您的问题在于使用了不可靠的方法来关闭映射的字节缓冲区(调用System.gc()System.runFinalization()一百次并不能保证您获得任何东西)。不幸的是,在Java API中没有可靠的方法来实现这一点,但在Sun JVM上(可能也适用于其他某些JVM),您可以使用以下代码:
public void unmapMmaped(ByteBuffer buffer) {
  if (buffer instanceof sun.nio.ch.DirectBuffer) {
    sun.misc.Cleaner cleaner = ((sun.nio.ch.DirectBuffer) buffer).cleaner();
    cleaner.clean();
  }
}

当然,这取决于JVM,并且如果Sun决定以不兼容的方式更改sun.nio.ch.DirectBuffersun.misc.Cleaner,您应该准备好修复您的代码(但实际上我不相信这会发生)。

3

这是对前面答案的补充,之前已经完全正确。

JDK 1.7抱怨使用sun.misc.Cleaner,表示该命名空间中的类不是JDK的正式部分,可能在未来消失。然而,截至1.7,它们仍然存在。

如果.clean()方法无法使用,则可以使用System.gc()作为备用方法,但必须承认这是一种“hack”,因此必须小心使用。

虽然System.gc()不能强制关闭未引用的映射,但实际上它通常会导致清理发生。在32位Linux(和Solaris)上的经验表明,缓冲区在第一次或第二次调用System.gc()期间的每个测试期间都会被释放。但是,在Windows上的行为是不同的。在大多数情况下,所有映射都在第二次调用System.gc()结束时释放,但有时需要3次调用。仍然有些情况需要更多的调用,需要更高数量的调用频率降低。这可能是具有迷惑性的,因为测试可能表明只需要4个调用,但一个月后就会失败。然后5个电话似乎足够了,但会在6个月后导致失败。

通过在FileChannel.truncate()周围使用try/catch块,并循环重新尝试操作来测试映射是否被释放。循环不能无限,因为有病理情况,特定堆配置将导致垃圾回收器永远不清除映射。但是,大约10次的循环几乎可以涵盖所有情况。如果对象在那时还没有消失,那么它就不会消失,应用程序将不得不放弃。这似乎是不充分的,但实际上极不可能,在不支持清洁工具的JVM上才会出现问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接