2011-08-31 33 views
14

Tengo un archivo de 40MB en el disco y necesito "asignarlo" a la memoria mediante una matriz de bytes.Java: ByteArrayOutputStream eficiente en memoria

Al principio, pensé que escribir el archivo en un ByteArrayOutputStream sería la mejor manera, pero creo que se necesitan aproximadamente 160 MB de espacio en el montón en algún momento durante la operación de copia.

¿Alguien sabe una mejor manera de hacer esto sin utilizar tres veces el tamaño de archivo de RAM?

Actualización: Gracias por la respuesta. Me di cuenta de que podía reducir un poco el consumo de memoria diciéndole a ByteArrayOutputStream que el tamaño inicial era un poco mayor que el tamaño del archivo original (usando el tamaño exacto con mi código obliga a la reasignación, tengo que comprobar por qué).

Hay otro punto de alta memoria: cuando obtengo byte [] con ByteArrayOutputStream.toByteArray. Echando un vistazo a su código fuente, puedo ver que es la clonación de la matriz:

public synchronized byte toByteArray()[] { 
    return Arrays.copyOf(buf, count); 
} 

Estoy pensando tan sólo pudiera extender ByteArrayOutputStream y reescribir este método, por lo que para volver directamente a la matriz original. ¿Hay algún peligro potencial aquí, dado el flujo y la matriz de bytes no se utilizará más de una vez?

+0

Pregunta similar http://stackoverflow.com/questions/964332/java-large-files-disk-io-performance – Santosh

Respuesta

13

MappedByteBuffer podría ser lo que estás buscando.

Me sorprende que tome tanta RAM para leer un archivo en la memoria, sin embargo. ¿Ha construido el ByteArrayOutputStream con una capacidad adecuada? Si no lo has hecho, la transmisión podría asignar una nueva matriz de bytes cuando esté cerca del final de los 40 MB, lo que significa que, por ejemplo, tendrías un buffer completo de 39MB y un nuevo buffer de dos veces el tamaño. Mientras que si la transmisión tiene la capacidad adecuada, no habrá reasignación (más rápido) y no se desperdiciará memoria.

+0

Gracias por su respuesta. Traté de configurar la capacidad adecuada, y el resultado fue el mismo. Para esto, preferiría algo basado en transmisiones, ya que sería interesante para mí aplicar algunos filtros. Sin embargo, si no hay otra manera, trataría de usar esos MappedByteBuffers. – user683887

5

Si realmente desea map el archivo en la memoria, entonces un FileChannel es el mecanismo apropiado.

Si todo lo que quiere hacer es leer el archivo en un simple byte[] (y no necesitan cambios a la matriz que se refleja de vuelta al archivo), entonces simplemente leer en un tamaño adecuado byte[] de una normal de FileInputStream debería ser suficiente.

Guava tiene Files.toByteArray() que hace todo eso por usted.

+0

La guayaba es la mejor elección para este problema. Gracias. – danik

10

ByteArrayOutputStream debería estar bien, siempre que especifique un tamaño apropiado en el constructor. Todavía creará una copia cuando llame al toByteArray, pero eso es solo temporal. ¿Realmente te importa la memoria brevemente subiendo mucho?

Alternativamente, si ya conoce el tamaño para empezar, puede simplemente crear una matriz de bytes y leer repetidamente desde un FileInputStream en ese búfer hasta que tenga todos los datos.

+0

Sí, es temporal, pero prefiero no usar tanta memoria. No sé cuán grandes serán algunos archivos, y esto puede usarse en máquinas pequeñas, así que trato de usar la menor cantidad de memoria posible. – user683887

+0

@ user683887: Entonces, ¿qué tal crear la segunda alternativa que presenté? Eso solo requerirá la cantidad de datos que se requiera.Si necesita aplicar filtros, siempre podría leer el archivo dos veces: una para calcular el tamaño que necesita y otra vez para leer los datos. –

2

Si tiene 40 MB de datos, no veo ningún motivo por el que se necesitarían más de 40 MB para crear un byte []. Supongo que está usando un ByteArrayOutputStream en crecimiento que crea una copia byte [] cuando termina.

Puede probar el viejo enfoque de leer el archivo a la vez.

File file = 
DataInputStream is = new DataInputStream(FileInputStream(file)); 
byte[] bytes = new byte[(int) file.length()]; 
is.readFully(bytes); 
is.close(); 

El uso de un MappedByteBuffer es más eficiente y evita una copia de los datos (o usando el montón mucho) siempre se puede utilizar el ByteBuffer directamente, por eso si usted tiene que utilizar un byte [] es poco probable a ayudar mucho.

2

... pero me parece que toma alrededor de 160 MB de espacio de almacenamiento dinámico en algún momento durante la operación de copia

Me parece extremadamente sorprendente ... en la medida en que tengo mis dudas de que se están midiendo el uso del montón correctamente.

Vamos a suponer que el código es algo como esto:

BufferedInputStream bis = new BufferedInputStream(
     new FileInputStream("somefile")); 
ByteArrayOutputStream baos = new ByteArrayOutputStream(); /* no hint !! */ 

int b; 
while ((b = bis.read()) != -1) { 
    baos.write((byte) b); 
} 
byte[] stuff = baos.toByteArray(); 

Ahora la forma en que un ByteArrayOutputStream gestiona su memoria intermedia es asignar un tamaño inicial, y (al menos) el doble de la memoria intermedia cuando se llena hasta . Por lo tanto, en el peor de los casos, baos podría usar hasta 80Mb de búfer para contener un archivo de 40Mb.

El paso final asigna una nueva matriz de exactamente baos.size() bytes para contener los contenidos del búfer. Eso es 40Mb. Por lo tanto, la cantidad máxima de memoria que está realmente en uso debería ser 120Mb.

Entonces, ¿dónde están esos 40Mb extra que se utilizan? Supongo que no, y que en realidad está informando sobre el tamaño total del montón, no sobre la cantidad de memoria ocupada por objetos alcanzables.


¿Cuál es la solución?

  1. Puede utilizar un búfer asignado en la memoria.

  2. Puede dar una sugerencia de tamaño cuando asigna el ByteArrayOutputStream; p.ej.

    ByteArrayOutputStream baos = ByteArrayOutputStream(file.size()); 
    
  3. Se podría prescindir de la ByteArrayOutputStream por completo y leer directamente en una matriz de bytes.

    byte[] buffer = new byte[file.size()]; 
    FileInputStream fis = new FileInputStream(file); 
    int nosRead = fis.read(buffer); 
    /* check that nosRead == buffer.length and repeat if necessary */ 
    

Ambas opciones 1 y 2 deben tener un uso de memoria máxima de 40Mb mientras lee un archivo de 40Mb; es decir, no hay espacio perdido.


Sería útil si publicó su código y describió su metodología para medir el uso de memoria.


Estoy pensando tan sólo pudiera extender ByteArrayOutputStream y reescribir este método, por así devolver la matriz original directamente. ¿Hay algún peligro potencial aquí, dado el flujo y la matriz de bytes no se utilizará más de una vez?

El potencial peligro es que sus suposiciones son incorrectas, o vuelven incorrecta debido a que otra modificación de su código sin darse cuenta ...

+0

Gracias, @Stephen. Tenías razón, el uso adicional del montón se debía a una inicialización incorrecta del tamaño BAOS, como describí en mi actualización. Estoy usando visualvm para medir el uso de la memoria: no estoy seguro de si es el mejor enfoque. – user683887

1

Para una explicación del comportamiento de crecimiento del buffer de ByteArrayOutputStream, lea this answer.

En respuesta a su pregunta, es es seguro para extender ByteArrayOutputStream. En su caso, probablemente sea mejor anular los métodos de escritura, de manera que la asignación adicional máxima esté limitada, por ejemplo, a 16 MB. No debe anular el toByteArray para exponer el miembro buf [] protegido. Esto se debe a que una secuencia no es un buffer; Una secuencia es un buffer que tiene un puntero de posición y protección de límite. Por lo tanto, es peligroso acceder y manipular potencialmente el búfer desde fuera de la clase.

1

Google Guava ByteSource parece ser una buena opción para almacenar en la memoria. A diferencia de las implementaciones como ByteArrayOutputStream o ByteArrayList (de la Biblioteca Colt), no fusiona los datos en una gran matriz de bytes, sino que almacena cada fragmento por separado. Un ejemplo:

List<ByteSource> result = new ArrayList<>(); 
try (InputStream source = httpRequest.getInputStream()) { 
    byte[] cbuf = new byte[CHUNK_SIZE]; 
    while (true) { 
     int read = source.read(cbuf); 
     if (read == -1) { 
      break; 
     } else { 
      result.add(ByteSource.wrap(Arrays.copyOf(cbuf, read))); 
     } 
    } 
} 
ByteSource body = ByteSource.concat(result); 

El ByteSource se puede leer como un InputStream en cualquier momento después:

InputStream data = body.openBufferedStream(); 
2

Estoy pensando tan sólo pudiera extender ByteArrayOutputStream y reescribir este método, por lo que volver a la matriz original directamente. ¿Hay algún peligro potencial aquí, dado el flujo y la matriz de bytes no se utilizará más de una vez?

No debe cambiar el comportamiento especificado del método existente, pero está perfectamente bien agregar un nuevo método. Aquí está una implementación:

/** Subclasses ByteArrayOutputStream to give access to the internal raw buffer. */ 
public class ByteArrayOutputStream2 extends java.io.ByteArrayOutputStream { 
    public ByteArrayOutputStream2() { super(); } 
    public ByteArrayOutputStream2(int size) { super(size); } 

    /** Returns the internal buffer of this ByteArrayOutputStream, without copying. */ 
    public synchronized byte[] buf() { 
     return this.buf; 
    } 
} 

Una alternativa que forma hacker de conseguir el buffer de cualquier ByteArrayOutputStream es usar el hecho de que su método writeTo(OutputStream) pasa el buffer directamente al OutputStream siempre que:

/** 
* Returns the internal raw buffer of a ByteArrayOutputStream, without copying. 
*/ 
public static byte[] getBuffer(ByteArrayOutputStream bout) { 
    final byte[][] result = new byte[1][]; 
    try { 
     bout.writeTo(new OutputStream() { 
      @Override 
      public void write(byte[] buf, int offset, int length) { 
       result[0] = buf; 
      } 

      @Override 
      public void write(int b) {} 
     }); 
    } catch (IOException e) { 
     throw new RuntimeException(e); 
    } 
    return result[0]; 
} 

(Eso funciona, pero no estoy seguro si es útil, dado que la subclase ByteArrayOutputStream es más simple.)

Sin embargo, por el resto de su pregunta, suena lik e todo lo que desea es un simple byte[] de los contenidos completos del archivo. A partir de Java 7, la manera más simple y rápida de hacerlo es llamando al Files.readAllBytes. En Java 6 y abajo, puede usar DataInputStream.readFully, como en Peter Lawrey's answer. De cualquier manera, obtendrá una matriz que está asignada una vez en el tamaño correcto, sin la reasignación repetida de ByteArrayOutputStream.

Cuestiones relacionadas