¿Cómo desasignar un archivo de la memoria asignada mediante FileChannel en java?
memory-mapped-files (11)
Estoy mapeando un archivo ("sample.txt") en la memoria usando FileChannel.map()
y luego cerrando el canal usando fc.close()
. Después de esto, cuando escribo en el archivo usando FileOutputStream, obtengo el siguiente error:
java.io.FileNotFoundException: sample.txt (La operación solicitada no puede formarse en un archivo con una sección mapeada por el usuario abierta)
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();
Supongo que esto puede deberse a que el archivo todavía está mapeado en la memoria incluso después de cerrar el FileChannel
. ¿Tengo razón? Si es así, ¿cómo puedo "desasignar" el archivo de la memoria? (No puedo encontrar ningún método para esto en la API). Gracias.
Editar: Parece que (agregando un método no asignado) fue enviado como RFE a Sun hace un tiempo: http://bugs.sun.com/view_bug.do?bug_id=4724038
Del MappedByteBuffer
javadoc:
Un búfer de bytes asignado y la asignación de archivos que representa siguen siendo válidos hasta que el búfer en sí mismo se recolecte.
Intenta llamar a System.gc()
? Incluso eso es solo una sugerencia para la VM.
El siguiente método estático podría ser utilizado:
public static void unmap(MappedByteBuffer buffer)
{
sun.misc.Cleaner cleaner = ((DirectBuffer) buffer).cleaner();
cleaner.clean();
}
Pero esta es una solución insegura debido a lo siguiente:
1) Conducir a fallas si alguien usa MappedByteBuffer después de quitar la asignación
2) Se basa en los detalles de implementación de MappedByteBuffer
Encontré información sobre unmap
, es un método de FileChannelImpl
y no accesible, por lo que puedes invocarlo mediante java reflect como:
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();
}
}
Es divertido ver tantas recomendaciones para hacer lo que el ítem 7 en ''Java efectivo'' específicamente dice que no se debe hacer. Un método de terminación como lo que hizo @Whome y no referencias al buffer es lo que se necesita. GC no puede ser forzado. Pero eso no impide que los desarrolladores lo intenten. Otra solución que encontré fue usar WeakReferences de 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();
}
La memoria mapeada se usa hasta que es liberada por el recolector de basura.
De los documentos de FileChannel
Una asignación, una vez establecida, no depende del canal de archivo que se utilizó para crearla. Cerrar el canal, en particular, no tiene ningún efecto sobre la validez del mapeo.
De MappedByteBuffer java doc
Un búfer de bytes asignado y la asignación de archivos que representa siguen siendo válidos hasta que el búfer en sí mismo se recolecte.
Así que sugeriría asegurar que no haya referencias restantes al buffer de byte mapeado y luego solicitar una recolección de basura.
La solución correcta aquí es usar try-with-resources.
Esto permite la creación del Canal y de los otros recursos para tener un alcance en un bloque. Una vez que el bloque sale, el canal y otros recursos se han ido y, por consiguiente, no se pueden usar (ya que nada tiene una referencia a ellos).
El mapeo de la memoria aún no se deshará hasta la próxima ejecución del GC, pero al menos no hay referencias colgantes.
Para evitar este error en Java, tuve que hacer lo siguiente, que funcionará bien para archivos pequeños a medianos:
// 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!!!!!
Si se puede garantizar que el objeto de búfer de archivo asignado sea elegible para la recolección de elementos no utilizados, no es necesario GC toda la máquina virtual para liberar la memoria asignada del búfer. Puede llamar System.runFinalization (). Esto llamará al método finalize () en el objeto de búfer de archivo asignado (si no hay referencias en los subprocesos de su aplicación) que liberará la memoria asignada.
Yo probaría JNI:
#ifdef _WIN32
UnmapViewOfFile(env->GetDirectBufferAddress(buffer));
#else
munmap(env->GetDirectBufferAddress(buffer), env->GetDirectBufferCapacity(buffer));
#endif
Incluye archivos: windows.h para Windows, sys / mmap.h para BSD, Linux, OSX.
[WinXP, SunJDK1.6] He tomado un mapa de ByteBuffer tomado de un canal de archivos. Después de leer SO, las publicaciones finalmente lograron llamar a un limpiador a través del reflejo sin ningún tipo de protección solar *. Ya no está bloqueado el bloqueo de archivos.
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
ByteBuffer cb = null;
try {
long size = fc.size();
cb = fc.map(FileChannel.MapMode.READ_ONLY, 0, size);
...do the magic...
finally {
try { fc.close(); } catch (Exception ex) { }
try { fis.close(); } catch (Exception ex) { }
closeDirectBuffer(cb);
}
private 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) { }
try {
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));
} catch(Exception ex) { }
cb = null;
}
Las ideas fueron tomadas de estos mensajes.
* ¿Cómo desasignar un archivo de la memoria asignada usando FileChannel en java?
* Ejemplos de forzar la liberación de memoria nativa directa ByteBuffer ha asignado, utilizando sun.misc.Unsafe?
* https://github.com/elasticsearch/elasticsearch/blob/master/src/main/java/org/apache/lucene/store/bytebuffer/ByteBufferAllocator.java#L40
sun.misc.Cleaner javadoc dice:
Limpiadores de base fantasma de uso general. Los limpiadores son una alternativa ligera y más robusta a la finalización. Son livianas porque no son creadas por la máquina virtual y, por lo tanto, no requieren que se cree una llamada ascendente JNI, y porque su código de limpieza es invocado directamente por la cadena del manejador de referencia en lugar de por la cadena del finalizador. Son más robustos porque usan referencias fantasmas, el tipo más débil de objeto de referencia, evitando así los desagradables problemas de ordenamiento inherentes a la finalización. Un limpiador rastrea un objeto referente y encapsula un trozo de código de limpieza arbitrario. Algún tiempo después de que el GC detecta que el referente de un limpiador se ha convertido en fantasma, el hilo del manejador de referencia ejecutará el limpiador. Los limpiadores también pueden invocarse directamente; son seguros para subprocesos y se aseguran de que ejecuten sus thunks a lo sumo una vez. Los limpiadores no son un reemplazo para la finalización. Deben usarse solo cuando el código de limpieza es extremadamente simple y directo. Los limpiadores no triviales son desaconsejables ya que corren el riesgo de bloquear el hilo del manejador de referencias y retrasar la limpieza y finalización.
Ejecutar System.gc () es una solución aceptable si el tamaño total de sus buffers es pequeño, pero si estuviera mapeando gigabytes de archivos trataría de implementar de esta manera:
((DirectBuffer) buffer).cleaner().clean()
¡Pero! Asegúrese de no acceder a ese búfer después de la limpieza o terminará con:
El entorno de ejecución de Java ha detectado un error fatal: EXCEPTION_ACCESS_VIOLATION (0xc0000005) en pc = 0x0000000002bcf700, pid = 7592, tid = 10184 Versión de JRE: Java (TM) SE Runtime Environment (8.0_40-b25) (compilación 1.8.0_40- b25) Java VM: Java HotSpot (TM) 64-Bit Server VM (25.40-b25 modo mixto windows-amd64 comprimido oops) Marco problemático: J 85 C2 java.nio.DirectByteBuffer.get (I) B (16 bytes) @ 0x0000000002bcf700 [0x0000000002bcf6c0 + 0x40] Error al escribir el volcado del núcleo. Los minivolcados no están habilitados de manera predeterminada en las versiones cliente de Windows. Un archivo de informe de error con más información se guarda como: C: / Users / ????? / Programs / testApp / hs_err_pid7592.log Método compilado (c2) 42392 85 4 java. nio.DirectByteBuffer :: get (16 bytes) total en el montón [0x0000000002bcf590,0x0000000002bcf828] = 664 reubicación [0x0000000002bcf6b0,0x0000000002bcf6c0] = 16 código principal [0x0000000002bcf6c0,0x0000000002bcf760] = código de código auxiliar 160
[0x0000000002bcf760,0x0000000002bcf778] = 24 uy
[0x0000000002bcf778,0x0000000002bcf780] = 8 metadatos
[0x0000000002bcf780,0x0000000002bcf798] = 24 datos de ámbitos
[0x0000000002bcf798,0x0000000002bcf7e0] = 72 scopes pcs
[0x0000000002bcf7e0,0x0000000002bcf820] = 64 dependencias
[0x0000000002bcf820,0x0000000002bcf828] = 8
¡Buena suerte!