2011-08-31 14 views
14

Ho un file da 40 MB nel disco e devo "mapparlo" in memoria usando un array di byte.Java: memoria efficiente ByteArrayOutputStream

In un primo momento, ho pensato che scrivere il file in un ByteArrayOutputStream sarebbe il modo migliore, ma trovo che occorra circa 160 MB di spazio heap in qualche momento durante l'operazione di copia.

Qualcuno conosce un modo migliore per farlo senza utilizzare tre volte la dimensione del file della RAM?

Aggiornamento: Grazie per le vostre risposte. Ho notato che potevo ridurre il consumo di memoria dicendo che una dimensione iniziale di ByteArrayOutputStream è leggermente superiore alla dimensione originale del file (utilizzando la dimensione esatta con il mio codice forza la riallocazione, è necessario controllare perché).

C'è un altro punto di memoria alta: quando ottengo il byte [] di nuovo con ByteArrayOutputStream.toByteArray. Dando uno sguardo al suo codice sorgente, posso vedere è la clonazione del matrice:

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

sto pensando ho potuto solo estendere ByteArrayOutputStream e riscrivere questo metodo, in modo per tornare direttamente l'array originale. C'è qualche potenziale pericolo qui, dato il flusso e l'array di byte non sarà usato più di una volta?

+0

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

risposta

13

MappedByteBuffer potrebbe essere quello che stai cercando.

Sono sorpreso che ci vuole così tanta RAM per leggere un file in memoria, però. Hai costruito il ByteArrayOutputStream con una capacità appropriata? In caso contrario, lo stream potrebbe allocare un nuovo array di byte quando si avvicina alla fine dei 40 MB, vale a dire che si disporrebbe, ad esempio, di un buffer completo di 39 MB e di un nuovo buffer con dimensioni doppie. Mentre se il flusso ha la capacità appropriata, non ci sarà alcuna riallocazione (più veloce) e nessuna memoria sprecata.

+0

Grazie per la risposta. Ho provato a impostare la capacità appropriata e il risultato è stato lo stesso. Per questo, preferirei qualcosa basato sui flussi, poiché sarebbe interessante per me applicare alcuni filtri. Tuttavia, se non c'è altro modo, proverei a usare quei MappedByteBuffer. – user683887

5

Se davvero si vuole mappa il file nella memoria, poi un FileChannel è il meccanismo appropriato.

Se tutto quello che vogliamo fare è leggere il file in un semplice byte[] (e non hanno bisogno di modifiche a tale matrice a riflettersi indietro al file), poi semplicemente leggere in un modo appropriato di dimensioni byte[] da un normale FileInputStream dovrebbe essere sufficiente

Guava ha Files.toByteArray() che fa tutto questo per voi.

+0

Guava è la scelta migliore per questo problema. Grazie. – danik

10

ByteArrayOutputStream dovrebbe essere a posto finché si specifica una dimensione appropriata nel costruttore. Creerà comunque una copia quando chiami toByteArray, ma è solo temporaneo. Ti dispiace davvero la memoria brevemente salendo molto?

In alternativa, se si conosce già la dimensione per iniziare, è sufficiente creare un array di byte e leggere ripetutamente da un FileInputStream in quel buffer finché non si hanno tutti i dati.

+0

Sì, è temporaneo, ma preferisco non usare così tanta memoria. Non so quanto saranno grandi i file, e questo può essere usato su macchine di piccole dimensioni, quindi cerco di usare il minor spazio possibile. – user683887

+0

@ user683887: Allora che ne dici di creare la seconda alternativa che ho presentato? Ciò richiederà solo la quantità di dati richiesta.Se è necessario applicare i filtri, è sempre possibile leggere il file due volte, una volta per calcolare le dimensioni necessarie, quindi nuovamente per leggere i dati. –

2

Se si dispone di 40 MB di dati, non vedo alcun motivo per cui sarebbero necessari più di 40 MB per creare un byte []. Presumo che tu stia utilizzando un ByteArrayOutputStream in crescita che crea una copia byte [] una volta terminato.

È possibile provare il vecchio leggere il file in una sola volta approccio.

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

Utilizzando un MappedByteBuffer è più efficiente ed evita una copia dei dati (o utilizzando il mucchio molto) a condizione che si può utilizzare direttamente il ByteBuffer, se si deve utilizzare un byte [] la sua improbabile per aiutare molto.

2

... ma trovo ci vogliono circa 160 MB di spazio di heap in qualche momento durante l'operazione di copia

Ho trovato questo estremamente sorprendente ... al punto che ho i miei dubbi che si stanno misurando l'utilizzo dell'heap correttamente.

Supponiamo che il codice è qualcosa di simile:

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

Ora il modo in cui un ByteArrayOutputStream gestisce la buffer è di assegnare una dimensione iniziale, e (almeno) raddoppiare il buffer quando si riempie in su . Pertanto, nel caso peggiore, baos potrebbe utilizzare un buffer fino a 80 Mb per contenere un file da 40 Mb.

Il passaggio finale alloca un nuovo array di esattamente baos.size() byte per contenere il contenuto del buffer. Quello è 40 Mb. Quindi la quantità massima di memoria effettivamente utilizzata dovrebbe essere di 120 Mb.

Quindi dove vengono utilizzati i 40Mb in più? La mia ipotesi è che non lo sono, e che in realtà stai segnalando la dimensione totale dell'heap, non la quantità di memoria che è occupata da oggetti raggiungibili.


Quindi qual è la soluzione?

  1. si potrebbe usare un buffer di memoria mappata.

  2. È possibile fornire un suggerimento per le dimensioni quando si assegna il valore ByteArrayOutputStream; per esempio.

    ByteArrayOutputStream baos = ByteArrayOutputStream(file.size()); 
    
  3. Si potrebbe rinunciare alla ByteArrayOutputStream tutto e leggere direttamente in un array di byte.

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

Entrambe le opzioni 1 e 2 dovrebbero avere un utilizzo della memoria di picco di 40Mb durante la lettura di un file 40Mb; non c'è spazio sprecato.


Sarebbe utile se il codice fosse stato pubblicato e descritto la metodologia per misurare l'utilizzo della memoria.


sto pensando ho potuto solo estendere ByteArrayOutputStream e riscrivere questo metodo, in modo da tornare direttamente l'array originale. C'è qualche potenziale pericolo qui, dato il flusso e l'array di byte non sarà usato più di una volta?

Il potenziale pericolo è che le vostre ipotesi sono corrette, o diventano corrette a causa di qualcun altro modificare il codice senza volerlo ...

+0

Grazie, @Stephen. Avevi ragione, l'utilizzo dell'heap aggiuntivo era dovuto a un'inizializzazione errata delle dimensioni di BAOS, come ho descritto nel mio aggiornamento. Sto usando visualvm per misurare l'utilizzo della memoria: non sono sicuro che sia l'approccio migliore. – user683887

1

Per una spiegazione del comportamento di crescita del buffer di ByteArrayOutputStream, leggere this answer.

In risposta alla tua domanda, è è sicuro estendere ByteArrayOutputStream. Nella tua situazione, è probabilmente meglio sovrascrivere i metodi di scrittura in modo tale che l'allocazione aggiuntiva massima sia limitata, ad esempio, a 16 MB. Non devi ignorare lo toByteArray per esporre il membro del buf [] protetto. Questo perché un flusso non è un buffer; Un flusso è un buffer che ha un puntatore di posizione e una protezione di confine. Quindi, è pericoloso accedere e potenzialmente manipolare il buffer dall'esterno della classe.

1

Google Guava ByteSource sembra essere una buona scelta per il buffering in memoria. A differenza delle implementazioni come ByteArrayOutputStream o ByteArrayList (dalla Libreria Colt) non unisce i dati in un enorme array di byte ma memorizza ogni chunk separatamente. Un esempio:

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

Il ByteSource può essere letta come un InputStream in qualsiasi momento dopo:

InputStream data = body.openBufferedStream(); 
2

sto pensando ho potuto solo estendere ByteArrayOutputStream e riscrivere questo metodo, in modo per restituire l'array originale direttamente. C'è qualche potenziale pericolo qui, dato il flusso e l'array di byte non sarà usato più di una volta?

Non è necessario modificare il comportamento specificato del metodo esistente, ma è perfettamente corretto aggiungere un nuovo metodo. Ecco un'implementazione:

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

Un'alternativa ma modo hackish di ottenere il buffer dalla qualsiasi ByteArrayOutputStream è utilizzare il fatto che il suo metodo writeTo(OutputStream) passa il buffer direttamente al OutputStream disponibile:

/** 
* 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]; 
} 

(Funziona, ma non sono sicuro se sia utile, dato che la sottoclasse di ByteArrayOutputStream è più semplice.)

Tuttavia, dal resto della tua domanda sembra un po ' e tutto ciò che vuoi è un semplice byte[] del contenuto completo del file. A partire da Java 7, il modo più semplice e veloce per farlo è chiamare Files.readAllBytes. In Java 6 e seguenti, è possibile utilizzare DataInputStream.readFully, come in Peter Lawrey's answer. In entrambi i casi, si otterrà un array assegnato una volta alla dimensione corretta, senza la ridistribuzione ripetuta di ByteArrayOutputStream.