如何正确关闭MappedByteBuffer?

9
这是我正在运行的代码:

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class Main {
    public static void main(String[] args) throws Exception {
        String filePath = "D:/temp/file";
        RandomAccessFile file = new RandomAccessFile(filePath, "rw");

        try {
            MappedByteBuffer buffer = file.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, 128);

            // Do something
            buffer.putInt(4);
        } finally {
            file.close();
            System.out.println("File closed");
        }

        System.out.println("Press any key...");
        System.in.read();

        System.out.println("Finished");
    }
}

在按键之前,我试图在FAR Manager中手动删除文件。但是,FAR提示该文件已被锁定:

 The process cannot access the file because it is being used by another process.
                     Cannot delete the file
                         D:\temp\file
                    Object is being opened in:
 Java(TM) Platform SE binary (PID: 5768, C:\Program Files\Java\jdk1.8.0_05\bin\javaw.exe)

只有按下一个键后,应用程序才会终止,然后我才能删除文件。

我的代码有什么问题?


1
在我的端上似乎可以工作。你尝试过查找任何正在运行的Java进程并通过在Windows上按Shift+Escape来关闭它们吗?或者关闭您可能已经打开并运行的文件的所有实例,然后再次运行它。 - Arno_Geismar
无济于事。我可以选择任意的文件(保证是新的),例如D:/temp/file2441,但仍然面临同样的问题。 - ZhekaKozlov
1
这实际上是一个Java的bug(https://bugs.openjdk.java.net/browse/JDK-4724038),目前似乎没有合适的解决方法。JDK团队认为到目前为止提供的所有答案都是危险的。 - Vlad
4个回答

7

@SANN3的答案在Java 9中不再适用。在Java 9中,有一个新方法sun.misc.Unsafe.invokeCleaner可以使用。以下是可行的代码:

MappedByteBuffer buffer = ...

// Java 9+ only:
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Object unsafe = unsafeField.get(null);
Method invokeCleaner = unsafeClass.getMethod("invokeCleaner", ByteBuffer.class);
invokeCleaner.invoke(unsafe, buffer);

2
这确实可行,但肯定有比使用反射更好的方法。听起来像是Java的一个bug/缺陷。 - Vlad
与JDK相关的错误(已关闭,标记为“不会修复”): https://bugs.openjdk.java.net/browse/JDK-6607535 基本上建议更改JDK,以便以共享模式打开文件,从而即使文件在使用中也可以被删除。 - Vlad
我继续尝试并创建了 https://github.com/vladak/RandomAccessFileTrap,它展示了这个问题,并且可以在相关的 Github actions 运行中看到解决方法的效果。 - Vlad
此外,我正在与Oracle的OpenJDK团队的一名工程师讨论,并且他正在查看。问题似乎出在未释放映射内存缓冲区上。话虽如此,不能保证是否会解决这个问题。 - Vlad
1
工程师追踪到了实际的错误:https://bugs.openjdk.java.net/browse/JDK-4724038 - 它实际上被标记为增强。引用错误中的话:“关于在JDC评论中描述的“解决方法”,即(滥用)反射以调用映射缓冲区的清理器对象:这是极其不可取的,委婉地说。强制取消映射对Java代码可见的映射字节缓冲区非常危险。这样做会危及系统的安全性和稳定性。” - Vlad
另外,错误报告中的另一个评论表明 sun.misc.Unsafe 将被移除。 - Vlad

7
试试这个。
public class Test
{
    public static void main(String[] args) throws Exception {
        String filePath = "D:/temp/file";
        RandomAccessFile file = new RandomAccessFile(filePath, "rw");
        FileChannel chan = file.getChannel();
        try {
            MappedByteBuffer buffer = chan.map(FileChannel.MapMode.READ_WRITE, 0, 128);

            // Do something
            buffer.putInt(4);
            buffer.force();
            Cleaner cleaner = ((sun.nio.ch.DirectBuffer) buffer).cleaner();
            if (cleaner != null) {
                cleaner.clean();
            }
        } finally {
            chan.close();
            file.close();
            System.out.println("File closed");
        }

        System.out.println("Press any key...");
        System.in.read();

        System.out.println("Finished");
    }
}

3
我猜问题是由于这个bug引起的(http://bugs.java.com/view_bug.do?bug_id=4715154):只要映射的缓冲区没有被垃圾回收,它就会阻止文件被删除。我猜清理器只是做了与垃圾回收期间发生的相同的事情... - gogognome
2
DirectBuffer不是内部API吗?它刚开始显示这样的错误。 - Line

0

如果您正在使用Java1.8,无法直接使用sun.nio.ch.DirectBuffer和Cleaner,则可以尝试:

public void clean(final ByteBuffer buffer) {
    AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
        try {
            Field field = buffer.getClass().getDeclaredField("cleaner");
            field.setAccessible(true);
            Object cleaner = field.get(buffer);

            Method cleanMethod = cleaner.getClass().getMethod("clean");
            cleanMethod.invoke(cleaner);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    });
}

0

这实际上是JDK的一个限制。自从JDK-4724038跟踪了这个问题(尽管它被标记为增强功能)并且在JDK中直接调用清理方法被强烈建议不要这样做(同时,Unsafe类可能会在未来的某个版本中消失),唯一的解决方法似乎是调用GC。如果使用try-with-resources处理文件,则代码如下:

try (RandomAccessFile file = new RandomAccessFile(filePath, "rw")) {
   MappedByteBuffer buffer = file.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, 128);
   // Do something
   buffer.putInt(4);
}
System.gc();  // has to be called outside the try-with-resources block

我创建了https://github.com/vladak/RandomAccessFileTrap来演示这个过程 - 前往该存储库的Github action选项卡中查看构建详细信息以查看实际结果。

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