53

我正在使用将文件(“sample.txt”)映射到内存FileChannel.map(),然后使用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 一段时间: http ://bugs.sun.com/view_bug.do?bug_id=4724038

4

13 回答 13

38

可以使用以下静态方法:

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

但这是不安全的解决方案,因为以下原因:
1)如果有人在取消映射后使用 MappedByteBuffer 会导致失败
2)它依赖于 MappedByteBuffer 实现细节

于 2011-02-17T23:25:59.153 回答
26

[WinXP,SunJDK1.6] 我有一个从文件通道获取的映射 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

于 2013-10-18T10:46:16.980 回答
15

MappedByteBufferjavadoc:

映射的字节缓冲区和它所代表的文件映射在缓冲区本身被垃圾收集之前保持有效。

试试打电话System.gc()?即使这只是对 VM 的建议。

于 2010-06-04T09:58:13.940 回答
5

sun.misc.Cleaner javadoc 说:

通用的基于幻象参考的清洁器。Cleaners 是一种轻量级且更强大的 finalization 替代方案。它们是轻量级的,因为它们不是由 VM 创建的,因此不需要创建 JNI 上行调用,而且它们的清理代码由引用处理程序线程直接调用,而不是由终结器线程调用。它们更健壮,因为它们使用幻像引用,最弱的引用对象类型,从而避免了终结固有的讨厌的排序问题。清理器跟踪引用对象并封装任意清理代码的 thunk。在 GC 检测到清理器的引用对象已变为幻象可访问后的一段时间,引用处理程序线程将运行清理器。也可以直接调用清洁器;它们是线程安全的,并确保它们最多运行一次 thunk。清洁工不能代替定稿。仅当清理代码非常简单明了时才应使用它们。不建议使用非平凡的清理器,因为它们可能会阻塞引用处理程序线程并延迟进一步的清理和完成。

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

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

但!确保在清洁后不要访问该缓冲区,否则您最终会得到:

Java 运行时环境检测到一个致命错误:EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x0000000002bcf700, pid=7592, tid=10184 JRE 版本:Java(TM) SE 运行时环境 (8.0_40-b25) (build 1.8.0_40- b25) Java VM:Java HotSpot(TM) 64 位服务器 VM(25.40-b25 混合模式 windows-amd64 压缩 oops)有问题的帧:J 85 C2 java.nio.DirectByteBuffer.get(I)B(16 字节)@ 0x0000000002bcf700 [0x0000000002bcf6c0+0x40] 无法写入核心转储。默认情况下,在 Windows 的客户端版本上不启用 Minidump 包含更多信息的错误报告文件保存为:C:\Users\?????\Programs\testApp\hs_err_pid7592.log 编译方法 (c2) 42392 85 4 java。 nio.DirectByteBuffer::get (16 bytes) 堆中的总数 [0x0000000002bcf590,0x0000000002bcf828] = 664 重定位 [0x0000000002bcf6b0,
[0x0000000002bcf760,0x0000000002bcf778] = 24个糟糕
[0x0000000002bcf778,0x0000000002bcf780] = 8元数据
[0x0000000002bcf780,0x0000000002bcf798] = 24个范围数据
[0x0000000002bcf798,0x0000000002bcf7e0] = 72范围件
[0x0000000002bcf7e0,0x0000000002bcf820] = 64依赖性
[0x0000000002bcf820,0x0000000002bcf828] = 8

祝你好运!

于 2015-07-23T16:21:54.537 回答
4

在不显示警告的情况下,其他答案中涵盖的方法((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 以及 JDK 9+ 上清理/关闭/取消映射 DirectByteBuffer/MappedByteBuffer 实例,但这不会给出反射警告:

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://stackoverflow.com/a/31592947/3950982
            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;
    }
}

请注意,您需要requires jdk.unsupported在 JDK 9+ 上的模块化运行时中将 添加到模块描述符(使用 时需要Unsafe)。

您的 jar 可能还需要RuntimePermission("accessClassInPackage.sun.misc"),RuntimePermission("accessClassInPackage.jdk.internal.misc")ReflectPermission("suppressAccessChecks").

在ClassGraph中实现了一个更完整的垃圾收集aMappedByteBuffer或aDirectByteBuffer的方法(我是作者)——入口点是closeDirectByteBuffer()末尾的方法FileUtils

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它,它将使 VM 崩溃。

此错误报告的最后一条评论中讨论了其他安全注意事项:

https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-4724038

于 2019-01-04T22:06:15.257 回答
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!!!!!
于 2012-01-19T09:22:15.877 回答
2

我发现了有关的信息unmap,它是一种方法,FileChannelImpl并且不可访问,因此您可以通过 java reflect 调用它,例如:

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();
    }
}
于 2017-05-09T07:20:09.303 回答
0

映射内存将一直使用,直到它被垃圾收集器释放。

来自FileChannel文档

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

来自MappedByteBuffer java 文档

映射的字节缓冲区和它所代表的文件映射在缓冲区本身被垃圾收集之前保持有效。

所以我建议确保没有对映射字节缓冲区的剩余引用,然后请求垃圾收集。

于 2010-06-04T09:59:06.727 回答
0

看到这么多建议去做“Effective Java”中第 7 项明确规定不要做的事情,这很有趣。需要像 @Whome 所做的那样的终止方法,并且不需要对缓冲区的引用。不能强制 GC。但这并不能阻止开发人员尝试。我发现的另一个解决方法是使用来自 http://jan.baresovi.cz/dr/en/java#memoryMap的 WeakReferences

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();
}
于 2017-08-23T23:50:35.760 回答
0

我会尝试 JNI:

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

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

于 2017-10-12T06:31:16.457 回答
0

试试https://github.com/real-logic/agrona

它的 IOUtil 类有一个unmap(MappedByteBuffer)方法可以完全满足您的需要。它允许显式地取消映射 MappedByteBuffer。

但是,它在sun.misc.Unsafe内部使用,但这与此处的其他答案没有任何不同。

于 2020-06-08T15:00:27.927 回答
-2

如果可以保证映射的文件缓冲区对象符合垃圾回收条件,则无需 GC 整个 VM 即可释放缓冲区的映射内存。您可以调用 System.runFinalization() 。这将调用映射文件缓冲区对象上的 finalize() 方法(如果它在您的应用程序线程中没有对它的引用),这将释放映射的内存。

于 2013-08-01T11:44:56.147 回答
-12

这里正确的解决方案是使用 try-with-resources。

这允许将通道和其他资源的创建限定为一个块。一旦块退出,通道和其他资源就消失了,随后就无法使用(因为没有任何东西可以引用它们)。

在下一次 GC 运行之前,内存映射仍然不会被撤消,但至少没有任何悬空引用。

于 2013-08-05T14:56:04.183 回答