web-dev-qa-db-ja.com

JavaでFileChannelを使用してマップされたメモリからファイルをマップ解除する方法は?

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でこれを行うためのメソッドが見つかりません)。ありがとう。

編集:it(unmapメソッドの追加)がRFEとしてSunに送信されたようです: http://bugs.Sun.com/view_bug.do?bug_id=4724038

41
learner135

MappedByteBuffer javadocから:

マップされたバイトバッファーとそれが表すファイルマッピングは、バッファー自体がガベージコレクションされるまで有効です。

System.gc()を呼び出してみてくださいそれもVMへの提案にすぎません。

11
Edward Dale

次の静的メソッドを使用できます。

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

しかし、次の理由により、これは安全でないソリューションです。
1)マッピング解除後に誰かがMappedByteBufferを使用すると失敗する
2)MappedByteBuffer実装の詳細に依存します

37
Timur Yusupov

[WinXP、SunJDK1.6] filechannelから取得したマップされたByteBufferを持っています。 SOの投稿を読んだ後、Sun。*パッケージをインポートせずに、リフレクションを介して最終的にクリーナーを呼び出すことができました。もはやファイルロックは残っていません。

editJDK9 +コード(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#L4

20
Whome

Sun.misc.Cleaner javadocは言う:

汎用ファントムリファレンスベースのクリーナー。クリーナーは、ファイナライズに代わる軽量で堅牢な代替手段です。 VMによって作成されないため、JNIアップコールを作成する必要がないため、およびクリーンアップコードが、参照ハンドラスレッドによってではなく、直接参照ハンドラスレッドによって呼び出されるため、軽量です。ファイナライザスレッド。最も弱いタイプの参照オブジェクトであるファントム参照を使用するため、より堅牢です。ファイナライゼーションに固有の厄介な順序付けの問題を回避します。参照オブジェクトを追跡し、任意のクリーンアップコードのサンクをカプセル化します。GCの後のしばらくクリーナーのリファレントがファントム到達可能になったことを検出すると、参照ハンドラースレッドがクリーナーを実行します。クリーナーは直接呼び出すこともできます。これらはスレッドセーフであり、サンクを最大1回実行することを保証します。クリーナーはファイナライズの代わりにはなりません。クリーンアップコードが非常に単純で単純な場合にのみ使用する必要があります。参照ハンドラスレッドをブロックし、その後のクリーンアップを遅延させるリスクがあるため、自明でないクリーナーはお勧めできません。そしてファイナライズ。

バッファーの合計サイズが小さい場合は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)(ビルド1.8.0_40-b25)Java VM:Java HotSpot(TM)64-Bit Server VM =(25.40-b25混合モードwindows-AMD64圧縮oops)問題のあるフレーム:J 85 C2 Java.nio.DirectByteBuffer.get(I)B(16バイト)@ 0x0000000002bcf700 [0x0000000002bcf6c0 + 0x40]コアダンプの書き込みに失敗しました。ミニダンプはそうではありませんWindowsのクライアントバージョンではデフォルトで有効化詳細情報を含むエラーレポートファイルは、C:\ Users\?????\Programs\testApp\hs_err_pid7592.logコンパイルメソッド(c2)42392 85 4 Java.nio.DirectByteBufferとして保存されます。 ::ヒープ内の合計(16バイト)を取得[0x0000000002bcf590,0x0000000002bcf828] = 664再配置[0x0000000002bcf6b0,0x0000000002bcf6c0] = 16メインコード[0x0000000002bcf6c0,0x0000000002bcf760] = 160スタブコード
[0x0000000002bcf760,0x0000000002bcf778] = 24 oop
[0x0000000002bcf778,0x0000000002bcf780] = 8メタデータ
[0x0000000002bcf780,0x0000000002bcf798] = 24個のスコープデータ
[0x0000000002bcf798,0x0000000002bcf7e0] = 72個のスコープ
[0x0000000002bcf7e0,0x0000000002bcf820] = 64個の依存関係
[0x0000000002bcf820,0x0000000002bcf828] = 8

幸運を!

5
Dmytro Voloshyn

この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
Mr Ed

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();
    }
}
1
饒夢楠

_((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")も必要になる場合があります。

1
Luke Hutchison

私は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
martins

マップされたメモリは、ガベージコレクターによって解放されるまで使用されます。

From FileChannel docs

いったん確立されたマッピングは、それを作成するために使用されたファイルチャネルに依存しません。特に、チャネルを閉じても、マッピングの有効性には影響しません。

From MappedByteBuffer Java doc

マップされたバイトバッファーとそれが表すファイルマッピングは、バッファー自体がガベージコレクションされるまで有効です。

したがって、マップされたバイトバッファーへの参照が残っていないことを確認してから、ガベージコレクションを要求することをお勧めします。

0
BenM

「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();
}
0
Droid Teahouse