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

时间:2010-06-04 09:47:00

标签: java memory-mapped-files

我使用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中找不到任何方法)。 感谢。

编辑: 看起来它(添加一个unmap方法)作为RFE提交给sun一段时间了: http://bugs.sun.com/view_bug.do?bug_id=4724038

13 个答案:

答案 0 :(得分:37)

可以使用以下静态方法:

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

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

答案 1 :(得分:17)

[WinXP,SunJDK1.6]我从filechannel获取了一个映射的ByteBuffer。在阅读了SO帖后,终于设法通过反射呼叫清洁工而没有任何太阳。*包进口。不再存在文件锁定。

编辑添加了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;
}

从这些帖子中获取了想法 * How to unmap a file from memory mapped using FileChannel in java?
* Examples of forcing freeing of native memory direct ByteBuffer has allocated, using sun.misc.Unsafe?
* https://github.com/elasticsearch/elasticsearch/blob/master/src/main/java/org/apache/lucene/store/bytebuffer/ByteBufferAllocator.java#L40

答案 2 :(得分:10)

来自MappedByteBuffer javadoc:

  

映射的字节缓冲区及其表示的文件映射在缓冲区本身被垃圾收集之前保持有效。

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

答案 3 :(得分:5)

sun.misc.Cleaner javadoc说:

  

基于幻像参考的通用清洁剂。   清洁剂是最终确定的轻量级和更强大的替代品。它们是轻量级的,因为它们不是由VM创建的,因此不需要创建JNI上行调用,并且因为它们的清理代码是由引用处理程序线程而不是终结器线程直接调用的。它们更强大,因为它们使用幻像引用,最弱的参考对象类型,从而避免了最终确定的令人讨厌的排序问题。   清理器跟踪引用对象并封装任意清理代码。在GC检测到清洁器的指示对象已变为幻像可达之后的一段时间,引用处理程序线程将运行清除程序。清洁工也可以直接调用;它们是线程安全的,并确保它们最多运行一次。   清洁工不是最终确定的替代品。只有在清理代码非常简单和直接时才应该使用它们。非平凡的清洁工是不可取的,因为它们有可能阻塞参考处理程序线程并延迟进一步的清理和最终化。

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

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

但是!确保在清洁后无法访问该缓冲区,或者最终得到:

  

Java运行时环境检测到致命错误:pc = 0x0000000002bcf700处的EXCEPTION_ACCESS_VIOLATION(0xc0000005),   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]写入失败   核心转储。默认情况下,在客户端版本上未启用Minidump   Windows包含更多信息的错误报告文件保存为:   C:\ Users \ ????? \ Programs \ testApp \ hs_err_pid7592.log编译方法   (c2)42392 85 4 java.nio.DirectByteBuffer :: get(16   堆中的总数[0x0000000002bcf590,0x0000000002bcf828] = 664   重定位[0x0000000002bcf6b0,0x0000000002bcf6c0] = 16主代码   [0x0000000002bcf6c0,0x0000000002bcf760] = 160存根码
  [0x0000000002bcf760,0x0000000002bcf778] = 24 oops
  [0x0000000002bcf778,0x0000000002bcf780] = 8个元数据
  [0x0000000002bcf780,0x0000000002bcf798] = 24个范围数据
  [0x0000000002bcf798,0x0000000002bcf7e0] = 72个范围pcs
  [0x0000000002bcf7e0,0x0000000002bcf820] = 64个依赖项
  [0x0000000002bcf820,0x0000000002bcf828] = 8

祝你好运!

答案 4 :(得分: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!!!!!

答案 5 :(得分:1)

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

来自FileChannel docs

  

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

来自MappedByteBuffer java doc

  

映射的字节缓冲区及其表示的文件映射在缓冲区本身被垃圾收集之前保持有效。

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

答案 6 :(得分:1)

我找到了有关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();
    }
}

答案 7 :(得分:0)

看到如此多的建议去做“有效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();
}

答案 8 :(得分: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。

答案 9 :(得分:0)

在其他答案中使用((DirectBuffer) byteBuffer).cleaner().clean()的方法,如果不显示An illegal reflective access operation has occurred警告,就不能在JDK 9+上运行(即使是反射形式)。在将来的某些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 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;
    }
}

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

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

答案 10 :(得分:0)

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

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

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

答案 11 :(得分:-2)

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

答案 12 :(得分:-12)

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

这允许创建频道&amp;要限制为块的其他资源。一旦该块退出,Channel&amp;其他资源已经消失了随后不能使用(因为没有任何参考)。

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