2012-03-06 6 views
9

Sto cercando di leggere un grande corpus di testo in memoria con Java. Ad un certo punto colpisce un muro e solo la spazzatura si raccoglie interminabilmente. Mi piacerebbe sapere se qualcuno ha esperienza a battere il GC di Java in sottomissione con set di dati di grandi dimensioni.Scarse prestazioni con elenchi Java di grandi dimensioni

Sto leggendo un file da 8 GB di testo inglese, in UTF-8, con una frase in una riga. Voglio split() ogni riga su spazio vuoto e archiviare gli array di stringhe risultanti in un ArrayList<String[]> per un'ulteriore elaborazione. Ecco un programma semplificato che mostra il problema:

/** Load whitespace-delimited tokens from stdin into memory. */ 
public class LoadTokens { 
    private static final int INITIAL_SENTENCES = 66000000; 

    public static void main(String[] args) throws IOException { 
     List<String[]> sentences = new ArrayList<String[]>(INITIAL_SENTENCES); 
     BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); 
     long numTokens = 0; 
     String line; 

     while ((line = stdin.readLine()) != null) { 
      String[] sentence = line.split("\\s+"); 
      if (sentence.length > 0) { 
       sentences.add(sentence); 
       numTokens += sentence.length; 
      } 
     } 
     System.out.println("Read " + sentences.size() + " sentences, " + numTokens + " tokens."); 
    } 
} 

Sembra piuttosto tagliato e asciutto, giusto? Noterai che ho anche pre-dimensionato il mio ArrayList; Ho poco meno di 66 milioni di frasi e 1,3 miliardi di token. Ora se tirate fuori il vostro Java object sizes di riferimento e la matita, troverete che dovrebbe richiedere circa:

  • 66e6 String[] riferimenti @ 8 byte EA = 0.5 GB
  • 66e6 String[] oggetti @ 32 byte EA = 2 GB
  • 66e6 char[] oggetti @ 32 byte ea = 2 GB
  • 1.3e9 String riferimenti @ 8 byte ea = 10 GB
  • 1.3e9 String s @ 44 bytes ea = 53 GB
  • 0.123,51641 milioni
  • 8e9 char s @ 2 byte ea = 15 GB

83 GB. (Noterai che ho davvero bisogno di usare dimensioni degli oggetti a 64 bit, dal momento che Compressed OOPs non mi può aiutare con> 32 GB di heap.) Siamo fortunati ad avere una macchina RedHat 6 con 128 GB di RAM, quindi accendo la mia VM server Java HotSpot (TM) a 64 bit (build 20.4-b02, modalità mista) dal mio kit Java SE 1.6.0_29 con pv giant-file.txt | java -Xmx96G -Xms96G LoadTokens per sicurezza, e mi rilasso mentre guardo top.

Da qualche parte a meno di metà dell'ingresso, con circa 50-60 GB di RSS, il garbage collector parallelo prende il 1300% della CPU (16 process box) e legge gli arresti di avanzamento. Poi passa ancora un po 'di GB, quindi il progresso si interrompe ancora più a lungo. Riempie 96 GB e non è ancora finito. L'ho lasciato andare per un'ora e mezza, e brucia solo il 90% del tempo di sistema per fare GC. Sembra estremo.

Per assicurarmi di non essere pazzo, ho montato l'equivalente Python (tutte e due le righe;) ed è stato completato in circa 12 minuti e 70 GB RSS.

Quindi: sto facendo qualcosa di stupido? (A parte il modo generalmente inefficiente di memorizzare le cose, che non posso davvero aiutare - e anche se le mie strutture dati sono grasse, a patto che si adattino, Java non dovrebbe solo soffocare.) C'è magia Consulenza GC per cumuli veramente grandi? Ho provato -XX:+UseParNewGC e sembra ancora peggio.

+0

Dove si trovano gli oggetti 'char []' che supportano le stringhe? –

+0

Negli oggetti 'String': intestazione oggetto a 24 byte + puntatore a 8 byte' char [] '+ 4 byte start, offset e hashcode, se i miei calcoli sono corretti. –

+0

Questo è il 'char []' * reference * - ma che dire degli stessi 'char []' * objects *? Un array 'char []' ha anche un overhead di oggetto ... –

risposta

3

-XX:+UseConcMarkSweepGC: finiture in 78 GB e ~ 12 minuti. (Quasi buono come Python!) Grazie per l'aiuto di tutti.

+0

Uso spesso CMS per server java con heap di grandi dimensioni per ridurre l'impatto di Gc sui tempi di risposta. Non ero convinto che la modifica della politica avrebbe aiutato il tuo codice in tale compito. Suppongo che l'uso di CMS abbia cambiato il modo in cui l'heap è suddiviso in parti e la JVM ottiene una OldGen più grande. –

2

Idea 1

partire dalla considerazione questo:

while ((line = stdin.readLine()) != null) { 

E almeno usato per essere il caso che readLine restituirebbe una String con un supporto char[] di almeno 80 caratteri.O se non che diventa un problema dipende da ciò che nella riga successiva fa:

String[] sentence = line.split("\\s+"); 

È necessario determinare se le stringhe restituite dal split mantenere lo stesso supporto char[].

Se lo fanno (e supponendo che le linee sono spesso più brevi di 80 caratteri) si dovrebbe usare:

line = new String(line); 

questo creerà un clone della copia della stringa con una matrice di stringhe "giuste dimensioni"

Se non, allora si dovrebbe potenzialmente lavorare fuori qualche modo di creare lo stesso comportamento ma cambiando in modo che fanno utilizzano lo stesso supporto char[] (vale a dire che sono sottostringhe della linea originale) - e fare la stessa operazione di clonazione azione, ovviamente. Non si desidera un separato char[] per parola, in quanto ciò sprecherà molta più memoria degli spazi.

Idea 2

il titolo parla lo scarso rendimento di liste - ma naturalmente si può facilmente prendere la lista fuori dall'equazione qui semplicemente creando un String[][], almeno per scopi di test. Sembra che tu sappia già la dimensione del file, e se non lo fai, puoi eseguirlo tramite wc per controllare in anticipo. Solo per vedere se è possibile evitare questo problema per iniziare con.

Idea 3

Quanti distinte parole ci sono nel vostro corpo? Hai considerato di mantenere un HashSet<String> e di aggiungere ogni parola ad esso mentre lo trovi? In questo modo si rischia di finire con lontane stringhe. A questo punto probabilmente vorrai abbandonare il "single backing char[] per riga" dalla prima idea: tu vorrai volere ogni stringa per essere supportata dal proprio array di caratteri, altrimenti una riga con una singola nuova parola in è richiederà comunque molti personaggi (In alternativa, per la vera e propria messa a punto, si poteva vedere quante "parole nuove" ci sono in una linea e clonare ogni stringa o no.)

+0

Oggetto: Idea 3, potresti prendere in considerazione l'uso di 'String.intern()'? –

+0

@LouisWasserman: Potenzialmente - ma solo se il processo non avrebbe fatto altro. Generalmente preferisco avere il mio set di internati, per evitare di "inquinare" il processo a livello di processo. (Anche se ci possono essere cose strane a significare che non è un problema in questi giorni. Semplicemente * sembra * più pulito.) –

+2

Hmmm. Suggerimento alternativo - Guava's ['Interners.newWeakInterner'] (http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/collect/Interners.html#newWeakInterner()) per farlo con riferimenti deboli, solo così le stringhe internate possono ottenere GC quando hai finito. –

2

Si consiglia di utilizzare i seguenti accorgimenti:

  • Aiuto la JVM per raccogliere gli stessi token in una singola stringa di riferimento grazie a sentences.add(sentence.intern()). Vedere String.intern per i dettagli. Per quanto ne so, dovrebbe anche avere l'effetto di cui parlava Jon Skeet, taglia il char array in piccoli pezzi.

  • Usa experimental HotSpot options alla stringa compatta e char [] implementazioni e quelli relativi:

    -XX:+UseCompressedStrings -XX:+UseStringCache -XX:+OptimizeStringConcat 
    

Con tale quantità di memoria, è necessario configurare il sistema e JVM per use large pages.

È davvero difficile migliorare le prestazioni con la sola regolazione GC e oltre il 5%.Dovresti innanzitutto ridurre il consumo di memoria delle applicazioni grazie alla creazione di profili.

A proposito, mi chiedo se è davvero necessario avere tutto il contenuto di un libro in memoria - non so quale codice seguirà con tutte le frasi ma dovresti considerare un'opzione alternativa come Lucene indexing tool per contare le parole o estraendo qualsiasi altra informazione dal tuo testo.

+0

Grazie per i suggerimenti. Ho provato l'internamento di String nelle app precedenti; diventa molto lento con molti dati e richiede un enorme PermGen, che confonde davvero GC. Ho provato le opzioni di ottimizzazione di String, e potrebbe aver diminuito un po 'l'uso della memoria, ma alla fine riempie memoria e borks. L'idea delle grandi pagine è buona; sfortunatamente, devi davvero riavviare per ottenere abbastanza memoria libera contigua (cosa è questo, DOS?;), e quella memoria non può essere usata per nient'altro. Sto leggendo su GC tuning, e penso che proverò il prossimo collezionista. –

0

È necessario verificare il modo in cui lo spazio dell'heap è suddiviso in parti (PermGen, OldGen, Eden e Survivors) grazie a VisualGC che ora è un plug-in per VisualVM.

Nel tuo caso, probabilmente vuole ridurre l'Eden e sopravvissuti per aumentare l'OldGen in modo che il GC non gira nella raccolta di un'OldGen completa ...

Per farlo, è necessario utilizzare le opzioni avanzate come :

-XX:NewRatio=2 -XX:SurvivorRatio=8 

Attenzione a queste zone e la relativa politica di allocazione predefinita dipende dal raccoglitore che si utilizza. Quindi cambia un parametro alla volta e ricontrolla.

Se tutto ciò che stringa dovrebbe vivere nella memoria di tutti i livetime JVM, è una buona idea per loro internalizzazione in PermGen definito sufficientemente grande, con -XX:MaxPermSize e per evitare la raccolta in quella zona, grazie alla -Xnoclassgc.

Vi consiglio di attivare queste opzioni di debug (senza spese generali del previsto) e, infine, inviare il registro GC in modo che possiamo avere un'idea della vostra attività GC.

-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:verbosegc.log 
+0

Stavo guardando questo, e potrei fare un tentativo. Grazie per il suggerimento. –