web-dev-qa-db-fra.com

Comment démapper un fichier de la mémoire mappé à l'aide de FileChannel en java?

Je mappe un fichier ("sample.txt") vers la mémoire à l'aide de FileChannel.map(), puis ferme le canal à l'aide de fc.close(). Ensuite, lorsque j'écris dans le fichier à l'aide de FileOutputStream, j'obtiens le message d'erreur suivant:

Java.io.FileNotFoundException: sample.txt (L'opération demandée ne peut pas être effectuée sur un fichier avec une section user-mapped ouverte)

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();

Je présume que cela peut être dû au fait que le fichier est toujours mappé sur la mémoire même après la fermeture de la FileChannel. Ai-je raison?. Si c'est le cas, comment puis-je "dé-cartographier" le fichier de la mémoire? (Je ne trouve aucune méthode à cet effet dans l'API) . Merci.

Edit: On dirait que (l’ajout d’une méthode unmap) a été soumis à Sun en tant que RFE: http://bugs.Sun.com/view_bug.do?bug_id=4724038

38
learner135

De la javadoc MappedByteBuffer:

Un tampon d'octets mappé et le mappage de fichiers qu'il représente restent valables jusqu'à ce que le tampon soit ramassé. 

Essayez d'appeler System.gc()? Même ce n'est qu'une suggestion à la VM.

9
Edward Dale

La méthode statique suivante peut être utilisée:

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

Mais c'est une solution dangereuse pour les raisons suivantes:
1) Mène à des échecs si quelqu'un utilise MappedByteBuffer après unmap
2) Il s’appuie sur les détails d’implémentation de MappedByteBuffer 

36
Timur Yusupov

[WinXP, SunJDK1.6] Un ByteBuffer mappé a été pris dans filechannel. Après avoir lu SO, les messages ont finalement réussi à appeler un nettoyeur par réflexion sans aucune importation de package Sun. *. Le verrouillage de fichier n'est plus en attente.

edit Ajouté le code 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;
}

Des idées ont été prises de ces messages.
* Comment démapper un fichier de la mémoire mappée à l’aide de FileChannel en java?
* Exemples d’imputation forcée de la libération de la mémoire native directement attribuée par ByteBuffer, à l’aide de Sun.misc.Unsafe?
* https://github.com/elasticsearch/elasticsearch/blob/master/src/main/Java/org/Apache/lucene/store/bytebuffer/ByteBufferAllocator.Java#L40

17
Whome

Javadoc Sun.misc.Cleaner dit:

Nettoyants universels à base de références fantômes. Les nettoyants constituent une alternative légère et plus robuste à la finalisation. Ils sont légers car ils ne sont pas créés par VM et ne nécessitent donc pas la création d'un appel ascendant JNI, et parce que leur code de nettoyage est appelé directement par le thread de gestionnaire de référence plutôt que par le thread de finaliseur. Ils sont plus robustes car ils utilisent des références fantômes, le type d’objet de référence le plus faible, évitant ainsi les problèmes de classement désagréables inhérents à la finalisation. Un nettoyeur suit un objet référent et encapsule une quantité de code de nettoyage arbitraire. Quelque temps après que le CPG détecte que le référent du nettoyeur est devenu fantôme-accessible, le thread du gestionnaire de référence exécutera le nettoyeur. Les nettoyants peuvent également être invoqués directement; ils sont thread-safe et veillent à exécuter leurs thunks au plus une fois. Les nettoyants ne remplacent pas la finalisation. Ils ne doivent être utilisés que lorsque le code de nettoyage est extrêmement simple et direct. Les nettoyants non triviaux sont déconseillés car ils risquent de bloquer le thread du gestionnaire de référence et de retarder le nettoyage et la finalisation. 

Exécuter System.gc () est une solution acceptable si la taille totale de vos tampons est petite, mais si je mappais des giga-octets de fichiers, j'essaierais de le mettre en œuvre de la manière suivante:

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

Mais! Assurez-vous de ne pas accéder à cette mémoire tampon après le nettoyage, sinon vous obtiendrez:

Une erreur fatale a été détectée par Java Runtime Environment: EXCEPTION_ACCESS_VIOLATION (0xc0000005) sur pc = 0x0000000002bcf700, pid = 7592, tid = 10184 Version JRE: environnement d'exécution Java (TM) SE (8.0_40-b25) (version 1.8.0_40-b25) VM virtuelle Java: Java HotSpot (TM) 64 bits Serveur VM (oops compressés Windows-AMD64 en mode mixte b.40 -25.5) Cadre problématique: J 85 C2 Java.nio.DirectByteBuffer.get (I) B (16 Octets) @ 0x0000000002bcf700 [0x0000000002bcf6c0 + 0x40] Échec d'écriture core dump. Les mini-pompes ne sont pas activées par défaut sur les versions clientes de Windows Un fichier de rapport d'erreur contenant plus d'informations est enregistré sous le nom: C:\Utilisateurs\?????\Programmes\testApp\hs_err_pid7592.log Méthode compilée (c2) 42392 85 4 total de Java.nio.DirectByteBuffer :: get (16 octets) dans le segment [0x0000000002bcf590,0x0000000002bcf828] = 664 relocalisation [0x0000000002bcf6b0,0x0000000002bcf6c0] = 16 code principal [0x0000000002bcf6c0,0x0000000002bcf760] = 160 code de remplacement
[0x0000000002bcf760,0x0000000002bcf778] = 24 oops
[0x0000000002bcf778,0x0000000002bcf780] = 8 métadonnées
[0x0000000002bcf780,0x0000000002bcf798] = 24 données d'étendues
[0x0000000002bcf798,0x0000000002bcf7e0] = 72 scopes
[0x0000000002bcf7e0,0x0000000002bcf820] = 64 dépendances
[0x0000000002bcf820,0x0000000002bcf828] = 8

Bonne chance!

5
Dmitrius

Pour contourner ce bogue en Java, je devais effectuer les opérations suivantes, qui fonctionneraient correctement pour les fichiers de taille petite à moyenne:

    // 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

J'ai trouvé des informations sur unmap, c'est une méthode de FileChannelImpl et elle n'est pas accessible. Vous pouvez donc l'invoquer avec 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();
    }
}
1
饒夢楠

La mémoire mappée est utilisée jusqu'à ce qu'elle soit libérée par le ramasse-miettes.

De FileChannel docs

Une fois établie, une correspondance ne dépend pas du canal de fichier utilisé pour la créer. La fermeture du canal, en particulier, n’a aucun effet sur la validité de la cartographie.

De MappedByteBuffer doc Java

Un tampon d'octets mappé et le mappage de fichiers qu'il représente restent valables jusqu'à ce que le tampon soit ramassé.

Je suggérerais donc de veiller à ce qu'il ne reste aucune référence au tampon d'octets mappé, puis de demander un ramassage des ordures.

1
BenM

Je voudrais essayer JNI:

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

Fichiers à inclure: windows.h pour Windows, sys/mmap.h pour BSD, Linux, OSX.

0
martins

La méthode décrite dans d'autres réponses qui utilise ((DirectBuffer) byteBuffer).cleaner().clean() ne fonctionne pas sur JDK 9+ (même sous forme réfléchissante) sans afficher un avertissement An illegal reflective access operation has occurred. Cela cessera de fonctionner dans certaines versions futures du JDK. Heureusement, Sun.misc.Unsafe.invokeCleaner(ByteBuffer) peut effectuer exactement le même appel sans avertissement: (source 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();
    }
}

Étant une classe Sun.misc, elle sera supprimée à un moment donné. Il est intéressant de noter que tous les appels sauf celui-ci dans Sun.misc.Unsafe sont envoyés directement à jdk.internal.misc.Unsafe (je ne sais pas pourquoi invokeCleaner(ByteBuffer) n'est pas envoyé par proxy de la même manière que toutes les autres méthodes - il s'agissait probablement d'une omission accidentelle).

J'ai écrit le code suivant qui est capable de nettoyer/fermer/annuler un mappage de DirectByteBuffer/MappedByteBuffer instances sur JDK 7/8, ainsi que JDK 9+, et cela ne donne pas l'avertissement de réflexion:

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;
    }
}

Notez que vous devrez ajouter le requires jdk.unsupported à votre descripteur de module dans une exécution modulaire sur JDK 9+ (nécessaire à l'utilisation de Unsafe).

Votre jarre peut aussi avoir besoin de RuntimePermission("accessClassInPackage.Sun.misc"), RuntimePermission("accessClassInPackage.jdk.internal.misc") et ReflectPermission("suppressAccessChecks").

0
Luke Hutchison

Il est amusant de voir autant de recommandations recommandant de ne pas faire l’article 7 de «Effective Java». Une méthode de terminaison comme celle utilisée par @Whome et aucune référence au tampon est nécessaire. GC ne peut pas être forcé… .. Mais cela n'empêche pas les développeurs d'essayer. Une autre solution que j’ai trouvée est d’utiliser WeakReferences from 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
Droid Teahouse