截断内存映射文件

我正在对索引文件使用内存映射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生产环境中,一切似乎都可以正常工作而没有警告,但文件保持原始大小。

任何人都可以解释为什么会发生这种情况和/或如何解决问题?


答案 1

您的问题是您正在使用不可靠的方法来关闭映射的字节缓冲区(对一百次调用并且不保证您任何事情)。不幸的是,Java API中没有可靠的方法来做到这一点,但是在Sun JVM上(也许在其他一些JVM上),您可以使用以下代码:System.gc()System.runFinalization()

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


答案 2

这只是对上一个答案的补充,这是完全正确的。

JDK 1.7 抱怨使用 ,说这个命名空间中的类不是 JDK 的正式部分,将来可能会消失。但是,截至1.7,它们仍然存在。sun.misc.Cleaner

如果该方法不可用,则 using 可以用作回退方法,但是必须确认这是一个“黑客”,因此必须小心谨慎。.clean()System.gc()

虽然不能强制关闭未引用的映射,但实际上它通常会导致清理发生。32 位 Linux(和 Solaris)上的经验显示,在每次测试期间,在第一次或第二次调用 期间都会释放缓冲区。但是,Windows 上的行为是不同的。在大多数情况下,所有映射都会在第二次调用结束时释放,但有时需要 3 次调用。在某些情况下,仍需要更多的呼叫,而对更多呼叫次数的要求在频率上会减少。这可能是欺骗性的,因为测试可能表明只需要4个电话,一个月后它就会失败。然后,5个电话可能看起来足够了,但结果在6个月内就失败了。System.gc()System.gc()System.gc()

测试地图是否已经释放可以通过在 周围使用一个块来完成,并带有一个循环来尝试失败时的操作。循环不可能是无限的,因为在一些病理情况下,特定的堆配置将导致垃圾回收器永远不会清理映射。但是,大约10个循环将涵盖几乎所有情况。如果到那时对象还没有消失,那么它就不会去任何地方,应用程序将不得不放弃。这可能看起来不够充分,但在实践中,这是极不可能的,并且只会是不支持清洁剂的JVM上的一个问题。try/catchFileChannel.truncate()