如何在Java中使用FileChannel取消映射内存中的文件?

56
我正在使用FileChannel.map()将文件("sample.txt")映射到内存中,然后使用fc.close()关闭通道。在此之后,当我使用FileOutputStream向文件写入时,出现以下错误:

java.io.FileNotFoundException: sample.txt(无法对已打开用户映射区的文件执行请求的操作)

File f = new File("sample.txt");
RandomAccessFile raf = new RandomAccessFile(f,"rw");
FileChannel fc = raf.getChannel();
MappedByteBuffer mbf = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
fc.close();
raf.close();

FileOutputStream fos = new FileOutputStream(f);
fos.write(str.getBytes());
fos.close();

我猜这可能是因为关闭了FileChannel后,文件仍然映射到内存中。我是对的吗?如果是这样,我该如何将文件从内存中“取消映射”?(我在API中找不到任何方法)。 谢谢。
编辑: 看起来(添加取消映射方法)曾经作为RFE提交给Sun: https://bugs.java.com/bugdatabase/view_bug?bug_id=4724038

2
我也曾经遇到过同样的问题。在我的情况下,我试图将数千个文件映射到内存中,结果导致了 OutOfMemoryError 错误,我认为这是文件句柄耗尽的结果。由于没有工具可以取消映射,这会导致不可预测的行为,即使在 OutOfMemoryError 时,映射会尝试执行 sleep(100)System.gc() - 它只会推迟痉挛的发生。 - dma_k
13个回答

38

可以使用以下静态方法:

public static void unmap(MappedByteBuffer buffer)
{
   sun.misc.Cleaner cleaner = ((DirectBuffer) buffer).cleaner();
   cleaner.clean();
}

但是这种方法是不安全的,因为存在以下问题:
1)在取消映射之后,如果有人继续使用MappedByteBuffer,则可能导致故障
2)它依赖于MappedByteBuffer的实现细节


3
这个“解决方案”不应该被使用。它既不具备可移植性,也存在安全隐患。 - kittylyst
29
问题不应该被解决。公司不应该销售其产品。开发者不应该得到报酬。 - stepancheg
@stephancheg 请看下面我的答案,了解如何(大部分)解决这个问题。 - kittylyst
4
如果再次访问缓冲区,可能会导致 JVM 崩溃。请参见注释:http://bugs.sun.com/view_bug.do?bug_id=4724038 - Aleksandr Dubinsky
该解决方案在某种程度上是“可移植的”,因为sun.misc.Cleaner存在于所有主要JDK版本(_Oracle JDK_,_OpenJDK_包括_Zulu_,IBM _J9_),除了_GCJ_和_Apache Harmony_。它还可能在JDK 1.9中得到保留(参见http://openjdk.java.net/jeps/260)。 - Bass
3
你也可以查看gflogger,了解DirectBuffer是如何“关闭”的,具体请参考这里 - Bass

27
[WinXP,SunJDK1.6] 我有一个从filechannel获取的映射的ByteBuffer。在阅读了SO帖子之后,最终通过反射调用清理程序,没有任何sun.*包导入。不再存在文件锁定问题。
编辑:添加了JDK9+代码(Luke Hutchison)。
private static void closeDirectBuffer(ByteBuffer cb) {
    if (cb==null || !cb.isDirect()) return;
    // we could use this type cast and call functions without reflection code,
    // but static import from sun.* package is risky for non-SUN virtual machine.
    //try { ((sun.nio.ch.DirectBuffer)cb).cleaner().clean(); } catch (Exception ex) { }

    // JavaSpecVer: 1.6, 1.7, 1.8, 9, 10
    boolean isOldJDK = System.getProperty("java.specification.version","99").startsWith("1.");  
    try {
        if (isOldJDK) {
            Method cleaner = cb.getClass().getMethod("cleaner");
            cleaner.setAccessible(true);
            Method clean = Class.forName("sun.misc.Cleaner").getMethod("clean");
            clean.setAccessible(true);
            clean.invoke(cleaner.invoke(cb));
        } else {
            Class unsafeClass;
            try {
                unsafeClass = Class.forName("sun.misc.Unsafe");
            } catch(Exception ex) {
                // jdk.internal.misc.Unsafe doesn't yet have an invokeCleaner() method,
                // but that method should be added if sun.misc.Unsafe is removed.
                unsafeClass = Class.forName("jdk.internal.misc.Unsafe");
            }
            Method clean = unsafeClass.getMethod("invokeCleaner", ByteBuffer.class);
            clean.setAccessible(true);
            Field theUnsafeField = unsafeClass.getDeclaredField("theUnsafe");
            theUnsafeField.setAccessible(true);
            Object theUnsafe = theUnsafeField.get(null);
            clean.invoke(theUnsafe, cb);
        }
    } catch(Exception ex) { }
    cb = null;
}

这些帖子提供了一些想法:
* 如何在Java中使用FileChannel进行内存映射后取消文件的映射?
* 使用sun.misc.Unsafe强制释放已分配的本地内存的直接ByteBuffer的例子?
* https://github.com/elasticsearch/elasticsearch/blob/master/src/main/java/org/apache/lucene/store/bytebuffer/ByteBufferAllocator.java#L40

适用于我们(JDK 7 64位,Red Hat)。它避免了在“sun”包上出现编译警告(我们始终在受控和预定义的环境中使用相同的Oracle VM)。 - xav
1
MappedByteBuffer、ByteBuffer和Buffer在HotSpot 8 Update 172中没有cleaner方法。然而,这个方法在DirectByteBuffer中是可用的。 - Nathan
1
在JDK 9+上,这将导致stderr上出现“发生了非法反射访问操作”的警告,这很丑陋且无法被抑制,并且表明这可能会在未来中断。请参见我的单独答案。 - Luke Hutchison
1
@LukeHutchison 谢谢,我在这段代码中进行了jdk9+的更改。我有一个非常古老的代码库,其中的库仍必须在JDK1.6中编译。我已在JDK1.6、JDK1.7、JDK1.8、OpenJDK10中进行了测试。 - Whome
1
你可能也需要在doPrivileged中包装它。(或者这只是为了支持使用SecurityManager?) 另请参阅我在模块化运行时中的答案中关于需要在模块描述符中添加“requires jdk.unsupported”语句以及在使用SecurityManager时需要反射权限的评论。 - Luke Hutchison
PS JDK 9和10已经“死亡”(不仅是不受支持,而是被弃用)--您应该在JDK 11上测试所有模块化代码。 - Luke Hutchison

15

根据 MappedByteBuffer 的 javadoc:

映射的字节缓冲和其表示的文件映射将保持有效,直到缓冲本身被垃圾回收。

尝试调用 System.gc()?即使那也只是向虚拟机提出建议。


非常感谢。我已经让它工作了,但是依赖于System.gc()会在不同的运行中产生意外的结果吗? - learner135
如果这解决了你的问题,那么至少你知道问题所在。现在你可以决定是要像疯子一样循环调用 System.gc(),还是重新考虑你的实现方案。 - Edward Dale
我在删除所有引用后尝试了System.gc(),但仍然出现错误。 - Tim Cooper

5

sun.misc.Cleaner javadoc 说明:

基于幽灵引用的通用清理器。Cleaners 是 finalization 的轻量级且更健壮的替代品。它们是轻量级的,因为它们不是由 VM 创建的,因此不需要 JNI upcall 来创建,并且因为它们的清理代码是直接由 reference-handler 线程调用而不是由 finalizer 线程调用,所以它们更加健壮。它们使用幽灵引用作为最弱类型的引用对象,因此避免了 finalization 固有的令人讨厌的排序问题。 一个 cleaner 跟踪一个 referent 对象并封装了一个任意清理代码的 thunk。在 GC 检测到清理器的 referent 成为幽灵可达之后,reference-handler 线程将运行该清理器。Cleaners 也可以直接调用;它们是线程安全的,并确保它们最多只运行一次其 thunks。 Cleaners 不是 finalization 的替代品。只有在清理代码非常简单明了的情况下才应该使用 cleaners。不建议使用复杂的 cleaners,因为它们会冒着阻塞 reference-handler 线程和延迟进一步清理和 finalization 的风险。

如果您的缓冲区总大小很小,则运行 System.gc() 是可以接受的解决方案,但如果我正在映射数千兆字节的文件,则会尝试像这样实现:

((DirectBuffer) buffer).cleaner().clean()

但是!确保在清理缓冲区后不要访问该缓冲区,否则您将会遇到以下问题:

Java运行时环境检测到致命错误:EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x0000000002bcf700, pid=7592, tid=10184 JRE版本:Java(TM) SE Runtime Environment (8.0_40-b25) (build 1.8.0_40-b25) Java VM: Java HotSpot(TM) 64-Bit Server VM (25.40-b25 mixed mode windows-amd64 compressed oops) 有问题的帧:J 85 C2 java.nio.DirectByteBuffer.get(I)B (16 bytes) @ 0x0000000002bcf700 [0x0000000002bcf6c0+0x40] 无法写入核心转储文件。默认情况下,在Windows客户端版本上未启用小型转储。 保存了一个包含更多信息的错误报告文件:C:\Users\?????\Programs\testApp\hs_err_pid7592.log 编译方法(c2) 42392 85 4 java.nio.DirectByteBuffer::get (16 bytes) total in heap [0x0000000002bcf590,0x0000000002bcf828] = 664 relocation [0x0000000002bcf6b0,0x0000000002bcf6c0] = 16 main code [0x0000000002bcf6c0,0x0000000002bcf760] = 160 stub code
[0x0000000002bcf760,0x0000000002bcf778] = 24 oops
[0x0000000002bcf778,0x0000000002bcf780] = 8 metadata
[0x0000000002bcf780,0x0000000002bcf798] = 24 scopes data
[0x0000000002bcf798,0x0000000002bcf7e0] = 72 scopes pcs
[0x0000000002bcf7e0,0x0000000002bcf820] = 64 dependencies
[0x0000000002bcf820,0x0000000002bcf828] = 8

祝你好运!


5

其他答案中涉及使用((DirectBuffer) byteBuffer).cleaner().clean()的方法在JDK 9+上不起作用(即使以反射形式),会显示An illegal reflective access operation has occurred警告。这将在某些未来的JDK版本中完全停止工作。幸运的是,sun.misc.Unsafe.invokeCleaner(ByteBuffer)可以为您执行完全相同的调用而无需警告:(来自OpenJDK 11源代码):

public void invokeCleaner(java.nio.ByteBuffer directBuffer) {
    if (!directBuffer.isDirect())
        throw new IllegalArgumentException("buffer is non-direct");

    DirectBuffer db = (DirectBuffer)directBuffer;
    if (db.attachment() != null)
        throw new IllegalArgumentException("duplicate or slice");

    Cleaner cleaner = db.cleaner();
    if (cleaner != null) {
        cleaner.clean();
    }
}

作为一个 sun.misc 类,它将在某个时候被移除。有趣的是,在 sun.misc.Unsafe 中除了这个方法之外的所有调用都直接代理到 jdk.internal.misc.Unsafe。我不知道为什么 invokeCleaner(ByteBuffer) 没有像其他所有方法一样被代理 -- 这可能是因为从 JDK 15 开始会有一种新的方式直接释放内存引用(包括 DirectByteBuffer 实例)。
我编写了以下代码,可以在 JDK 7/8 上清理/关闭/取消映射 DirectByteBuffer/MappedByteBuffer 实例,以及在 JDK 9+ 上运行,并且不会出现反射警告:
private static boolean PRE_JAVA_9 = 
        System.getProperty("java.specification.version","9").startsWith("1.");

private static Method cleanMethod;
private static Method attachmentMethod;
private static Object theUnsafe;

static void getCleanMethodPrivileged() {
    if (PRE_JAVA_9) {
        try {
            cleanMethod = Class.forName("sun.misc.Cleaner").getMethod("clean");
            cleanMethod.setAccessible(true);
            final Class<?> directByteBufferClass =
                    Class.forName("sun.nio.ch.DirectBuffer");
            attachmentMethod = directByteBufferClass.getMethod("attachment");
            attachmentMethod.setAccessible(true);
        } catch (final Exception ex) {
        }
    } else {
        try {
            Class<?> unsafeClass;
            try {
                unsafeClass = Class.forName("sun.misc.Unsafe");
            } catch (Exception e) {
                // jdk.internal.misc.Unsafe doesn't yet have invokeCleaner(),
                // but that method should be added if sun.misc.Unsafe is removed.
                unsafeClass = Class.forName("jdk.internal.misc.Unsafe");
            }
            cleanMethod = unsafeClass.getMethod("invokeCleaner", ByteBuffer.class);
            cleanMethod.setAccessible(true);
            final Field theUnsafeField = unsafeClass.getDeclaredField("theUnsafe");
            theUnsafeField.setAccessible(true);
            theUnsafe = theUnsafeField.get(null);
        } catch (final Exception ex) {
        }
    }
}

static {
    AccessController.doPrivileged(new PrivilegedAction<Object>() {
        @Override
        public Object run() {
            getCleanMethodPrivileged();
            return null;
        }
    });
}

private static boolean closeDirectByteBufferPrivileged(
            final ByteBuffer byteBuffer, final LogNode log) {
    try {
        if (cleanMethod == null) {
            if (log != null) {
                log.log("Could not unmap ByteBuffer, cleanMethod == null");
            }
            return false;
        }
        if (PRE_JAVA_9) {
            if (attachmentMethod == null) {
                if (log != null) {
                    log.log("Could not unmap ByteBuffer, attachmentMethod == null");
                }
                return false;
            }
            // Make sure duplicates and slices are not cleaned, since this can result in
            // duplicate attempts to clean the same buffer, which trigger a crash with:
            // "A fatal error has been detected by the Java Runtime Environment:
            // EXCEPTION_ACCESS_VIOLATION"
            // See: https://dev59.com/InA85IYBdhLWcg3wCe9Z#31592947
            if (attachmentMethod.invoke(byteBuffer) != null) {
                // Buffer is a duplicate or slice
                return false;
            }
            // Invoke ((DirectBuffer) byteBuffer).cleaner().clean()
            final Method cleaner = byteBuffer.getClass().getMethod("cleaner");
            cleaner.setAccessible(true);
            cleanMethod.invoke(cleaner.invoke(byteBuffer));
            return true;
        } else {
            if (theUnsafe == null) {
                if (log != null) {
                    log.log("Could not unmap ByteBuffer, theUnsafe == null");
                }
                return false;
            }
            // In JDK9+, calling the above code gives a reflection warning on stderr,
            // need to call Unsafe.theUnsafe.invokeCleaner(byteBuffer) , which makes
            // the same call, but does not print the reflection warning.
            try {
                cleanMethod.invoke(theUnsafe, byteBuffer);
                return true;
            } catch (final IllegalArgumentException e) {
                // Buffer is a duplicate or slice
                return false;
            }
        }
    } catch (final Exception e) {
        if (log != null) {
            log.log("Could not unmap ByteBuffer: " + e);
        }
        return false;
    }
}

/**
 * Close a {@code DirectByteBuffer} -- in particular, will unmap a
 * {@link MappedByteBuffer}.
 * 
 * @param byteBuffer
 *            The {@link ByteBuffer} to close/unmap.
 * @param log
 *            The log.
 * @return True if the byteBuffer was closed/unmapped (or if the ByteBuffer
 *            was null or non-direct).
 */
public static boolean closeDirectByteBuffer(final ByteBuffer byteBuffer,
            final Log log) {
    if (byteBuffer != null && byteBuffer.isDirect()) {
        return AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
            @Override
            public Boolean run() {
                return closeDirectByteBufferPrivileged(byteBuffer, log);
            }
        });
    } else {
        // Nothing to unmap
        return false;
    }
}

请注意,在JDK 9+的模块化运行时中,您需要将requires jdk.unsupported添加到您的模块描述符中(需要使用Unsafe)。
您的jar文件还可能需要RuntimePermission("accessClassInPackage.sun.misc")RuntimePermission("accessClassInPackage.jdk.internal.misc")ReflectPermission("suppressAccessChecks")
对于垃圾回收MappedByteBufferDirectByteBuffer,在ClassGraph(由我编写)中实现了更完整的方法——入口点是FileUtils末尾的closeDirectByteBuffer()方法: https://github.com/classgraph/classgraph/blob/latest/src/main/java/nonapi/io/github/classgraph/utils/FileUtils.java#L543 该代码使用反射编写,因为Java API(包括Unsafe)即将消失。
请注意,在JDK 16+中还存在另一个问题:
除非您使用NarcissusJVM-Driver库来绕过强封装,否则此代码将无法在JDK 16+中正常工作。这是因为MappedByteBuffer.clean()是一个私有方法,而JDK 16强制实施强封装。ClassGraph通过在运行时调用Narcissus或JVM-driver来反射抽象出对私有封装方法的访问: https://github.com/classgraph/classgraph/blob/latest/src/main/java/nonapi/io/github/classgraph/reflection/ReflectionUtils.java 警告:如果您在清理(释放)后尝试访问DirectByteBuffer,它将导致虚拟机崩溃。
还有其他安全注意事项,请参阅此错误报告中的最后一条评论: https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-4724038

2
为了解决Java中的这个错误,我必须采取以下措施,这对于小到中等大小的文件可以正常工作:
    // first open the file for random access
    RandomAccessFile raf = new RandomAccessFile(file, "r");

    // extract a file channel
    FileChannel channel = raf.getChannel();

    // you can memory-map a byte-buffer, but it keeps the file locked
    //ByteBuffer buf =
    //        channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());

    // or, since map locks the file... just read the whole file into memory
    ByteBuffer buf = ByteBuffer.allocate((int)file.length());
    int read = channel.read(buf);

    // .... do something with buf

    channel.force(false);  // doesn't help
    channel.close();       // doesn't help
    channel = null;        // doesn't help
    buf = null;            // doesn't help
    raf.close();           // try to make sure that this thing is closed!!!!!

2

我发现了有关unmap的信息,它是FileChannelImpl的一种方法,但不可访问,因此您可以通过Java反射调用它,如下:

public static void unMapBuffer(MappedByteBuffer buffer, Class channelClass) {
    if (buffer == null) {
        return;
    }

    try {
        Method unmap = channelClass.getDeclaredMethod("unmap", MappedByteBuffer.class);
        unmap.setAccessible(true);
        unmap.invoke(channelClass, buffer);
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }
}

由于Java 8中不存在MappedByteBuffer.unmap(),因此这将无法工作。 - Nathan
@Nathan unmap 不是 MappedByteBuffer 的方法,你可以在 FileChannelImpl 中找到它。实际上,这并不是一个真正适合 取消映射缓冲区 的方式。 - 饒夢楠

0

看到这么多建议去做《Effective Java》中明确不建议做的第7项,真是有趣。像@Whome所做的终止方法和对缓冲区的无引用是所需的。GC不能被强制执行。 但这并不能阻止开发人员尝试。我发现的另一个解决方法是使用http://jan.baresovi.cz/dr/en/java#memoryMap中的弱引用。

final MappedByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, size);
....
final WeakReference<mappedbytebuffer> bufferWeakRef = new WeakReference<mappedbytebuffer>(bb);
bb = null;

final long startTime = System.currentTimeMillis();
while(null != bufferWeakRef.get()) {
  if(System.currentTimeMillis() - startTime > 10)
// give up
    return;
    System.gc();
    Thread.yield();
}

0

我会尝试使用JNI:

#ifdef _WIN32
UnmapViewOfFile(env->GetDirectBufferAddress(buffer));
#else
munmap(env->GetDirectBufferAddress(buffer), env->GetDirectBufferCapacity(buffer));
#endif

包含文件:Windows 使用 windows.h,BSD、Linux、OSX 使用 sys/mmap.h。


0

映射内存在垃圾回收器释放之前一直被使用。

来自FileChannel文档

一旦建立了映射,它就不依赖于用于创建它的文件通道。特别是关闭通道对映射的有效性没有影响。

来自MappedByteBuffer java文档

映射的字节缓冲区及其表示的文件映射将保持有效,直到缓冲区本身被垃圾回收。

因此,我建议确保没有剩余对映射字节缓冲区的引用,然后请求进行垃圾回收。


1
没有办法强制进行垃圾回收,即使通过 System.gc() 也不行。 - aioobe
这里只是对一个语句所作的评论:“没有办法强制进行垃圾回收,甚至通过System.gc()也不行”。实际上是可以的。如果确保所有应用程序线程都处于空闲或睡眠状态,则System.gc()将每次运行GC,至少在Oracle的VM中是这样。 - user1353729
1
唯一强制进行垃圾回收的方法是引发 OutOfMemoryError。规范保证在抛出异常之前,GC 已经运行。 - Bombe
@user1353729 这只是一种经验观察,而不是普遍真理。Javadoc 中也没有任何相关内容。而且你提到的情况基本上是不可能实现的。 - user207421

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