2012-03-06 16 views
9

Próbuję odczytać duży korpus tekstowy do pamięci za pomocą Javy. W pewnym momencie uderza w ścianę i tylko śmieci zbierają się w nieskończoność. Chciałbym wiedzieć, czy ktokolwiek ma doświadczenie z pobieraniem GC Java do przesyłania z dużymi zbiorami danych.Słaba wydajność z dużymi listami Java

Czytam plik 8 GB tekstu w języku angielskim, w UTF-8, z jednym zdaniem do wiersza. Chcę split() każdej linii na białych znakach i przechowywać wynikowe tablice String w ArrayList<String[]> w celu dalszego przetwarzania. Oto uproszczony program, w którym występuje problem:

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

Wydaje się być dobrze przycięty i wysuszony, prawda? Zauważysz, że nawet wstępnie zmieniłem rozmiar mojego ArrayList; Mam niewiele mniej niż 66 milionów zdań i 1,3 miliarda żetonów. Teraz, jeśli bat na odniesienie Java object sizes i ołówka, przekonasz się, że powinno wymagać około:

  • 66e6 String[] referencje @ 8 bajtów EA = 0,5 GB
  • 66e6 String[] obiekty @ 32 bajty EA = 2 GB
  • 66e6 char[] obiekty @ 32 bajtów EA = 2 PL
  • 1.3e9 String odniesienia @ 8 bajtów EA = 10 PL
  • 1.3e9 String s @ 44 bajtów EA = 53 PL
  • 8E9 char S @ 2 bajty Ea = 15 PL

83 PL. (Zauważysz, że naprawdę potrzebuję używać 64-bitowych rozmiarów obiektów, ponieważ Compressed OOPs nie może mi pomóc z stertą o wielkości 32 GB.) Mamy szczęście mieć maszynę RedHat 6 z 128 GB pamięci RAM, więc odpalam moja 64-bitowa maszyna wirtualna Java HotSpot (kompilacja 20.4-b02, tryb mieszany) z mojego zestawu Java SE 1.6.0_29 z pv giant-file.txt | java -Xmx96G -Xms96G LoadTokens tylko po to, aby być bezpiecznym i cofnąć się, gdy oglądam top.

Mniej niż w połowie wprowadzania danych, przy około 50-60 GB RSS, równoległy moduł do zbierania śmieci uruchamia procesor do 1300% (skrzynka 16-procesowa) i zatrzymuje zatrzymanie postępu. Potem idzie o kilka więcej GB, a potem postęp zatrzymuje się jeszcze dłużej. Wypełnia 96 GB i nie jest jeszcze gotowy. Pozwoliłem, by trwało to półtorej godziny, i to po prostu spalenie ~ 90% czasu systemowego robi GC. To wydaje się ekstremalne.

Aby upewnić się, że nie jestem szalony, pobudziłem odpowiednik Pythona (wszystkie dwie linie;) i uruchomiono go do ukończenia w około 12 minut i 70 GB RSS.

Więc: czy robię coś głupiego? (Poza ogólnie nieefektywnym sposobem przechowywania rzeczy, których naprawdę nie mogę pomóc - i nawet jeśli moje struktury danych są grube, tak długo jak będą pasowały, Java nie powinna po prostu udusić.) Czy jest magia Porady GC dla naprawdę dużych stert? Spróbowałem -XX:+UseParNewGC i wygląda na to, że jest jeszcze gorzej.

+0

Gdzie są obiekty "char []" wspierające łańcuchy? –

+0

W obiektach "String": 24-bajtowy nagłówek obiektu + 8-bajtowy "char []" wskaźnik + 4-bajtowe początkowe, offsetowe i hashcode, jeśli moje obliczenia są poprawne. –

+0

To jest "char []" * * * - ale co z obiektami char [] * *? Tablica 'char []' ma również obiekt nadrzędny ... –

Odpowiedz

3

-XX:+UseConcMarkSweepGC: Wykończenie w 78 GB i ~ 12 minut. (Prawie tak dobry jak Python!) Dzięki za pomoc wszystkich.

+0

Często używam CMS dla serwera Java z dużą stertą, aby zmniejszyć wpływ gc na czas odpowiedzi. Nie byłam przekonana, że ​​zmiana polityki pomoże w twoim kodzie w takim zadaniu. Domyślam się, że używanie CMS zmieniło sposób dzielenia sterty na części i twoja JVM dostaje większy OldGen. –

2

Idea 1

start rozważając to:

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

To przynajmniej wykorzystywane być tak, że readLine zwróci String z podkładem char[] co najmniej 80 znaków.Czy ta staje się problemem, zależy od tego, co robi następny wiersz:

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

Należy ustalić, czy ciągi zwracane przez split zachować ten sam podkład char[].

Jeśli robią (i zakładając swoje linie często są krótsze niż 80 znaków) należy użyć:

line = new String(line); 

To stworzy klon kopię napisu z tablicy ciągów „właściwej wielkości”

Jeśli oni nIE, to należy ewentualnie wypracować jakiś sposób tworzenia samo zachowanie ale zmieniając go tak robią używać tego samego podłoża char[] (tj oni podciągi oryginalnej linii) - i wykonuj tę samą operację klonowania oczywiście. Nie potrzebujesz osobnego char[] na słowo, ponieważ straci to znacznie więcej pamięci niż miejsca.

Idea 2

tytuł mówi o słabej wydajności list - ale oczywiście można łatwo zrobić listę z równania tu po prostu tworząc String[][], przynajmniej dla celów testowych. Wygląda na to, że znasz już rozmiar pliku - a jeśli nie, możesz go uruchomić przez wc, aby sprawdzić wcześniej. Aby sprawdzić, czy możesz uniknąć tego problemu, należy rozpocząć od.

Idea 3

Ile odrębne słowa istnieją w korpusie? Czy zastanawiałeś się nad zachowaniem numeru HashSet<String> i dodaniem do niego każdego słowa, gdy go spotkasz? W ten sposób prawdopodobnie skończysz z daleko mniejszą liczbą ciągów. W tym momencie prawdopodobnie zechcesz zrezygnować z "pojedynczego podkładu char[] dla linii" od pierwszego pomysłu - chcesz, aby każdy ciąg był wspierany przez jego własną tablicę znaków, ponieważ w przeciwnym razie linia z pojedynczym nowym słowem jest nadal będzie wymagało wielu postaci. (Alternatywnie, dla prawdziwego dostrajania, można zobaczyć, jak wiele „nowych słów” nie są w linii i sklonować Każdy łańcuch czy nie).

+0

Re: Idea 3, czy mógłbyś rozważyć użycie 'String.intern()'? –

+0

@LouisWasserman: Potencjalnie - ale tylko wtedy, gdy proces nie miał nic więcej. Generalnie wolę mieć własny zestaw interwencyjny, aby uniknąć "zanieczyszczania" całego procesu. (Chociaż może być wiele ciekawych rzeczy, które obecnie nie stanowią problemu, to po prostu * czuje się * czystsze.) –

+2

Hmmm. Alternatywna sugestia - "Guava's [' Interners.newWeakInterner'] (http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/collect/Interners.html#newWeakInterner()), aby zrobić to ze słabymi referencjami, tak aby internowane łańcuchy mogły uzyskać GCd, gdy skończysz. –

2

Należy używać następujących sztuczki:

  • Pomoc JVM do zbierania tych samych tokenów do pojedynczego odwołania do String, dzięki sentences.add(sentence.intern()). Aby uzyskać szczegółowe informacje, patrz String.intern. O ile mi wiadomo, powinien on również wywoływać efekt, o którym mówił Jon Skeet, tnie układ char na małe kawałki.

  • Zastosowanie experimental HotSpot options kompaktowej ciąg i char [] wdrożeń i tych związanych z:

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

Z ilości takiej pamięci, należy skonfigurować system i JVM do use large pages.

Naprawdę trudno jest poprawić wydajność dzięki samemu tuningowi GC i ponad 5%.Najpierw należy zmniejszyć zużycie pamięci aplikacji dzięki profilowaniu.

Nawiasem mówiąc, zastanawiam się, czy naprawdę potrzebujesz uzyskać pełną zawartość książki w pamięci - nie wiem, co twój kod robi dalej ze wszystkimi zdaniami, ale powinieneś rozważyć alternatywną opcję, taką jak Lucene indexing tool, aby policzyć słowa lub wydobywanie wszelkich innych informacji z twojego tekstu.

+0

Dzięki za sugestie. Próbowałem interakcji String w poprzednich aplikacjach; robi się bardzo wolno z dużą ilością danych i wymaga ogromnego PermGenu, który naprawdę myli GC. Wypróbowałem twoje opcje optymalizacji String i może to nieco zmniejszyć użycie pamięci, ale nadal w końcu zapełnia pamięć i borks. Pomysł dużych stron jest dobry; niestety, naprawdę trzeba się zrestartować, aby uzyskać wystarczającą wolną pamięć ciągłą (co to jest, DOS?;), i że pamięć nie może być używana do niczego innego. Czytam o tuningu GC i myślę, że mam zamiar wypróbować kolejny kolektor. –

0

Powinieneś sprawdzić sposób dzielenia przestrzeni sterty na części (PermGen, OldGen, Eden i Survivors) dzięki VisualGC, która jest teraz wtyczką dla VisualVM.

W twoim przypadku, to prawdopodobnie chcą zmniejszyć Eden i ocalałych, aby zwiększyć OldGen tak, że GC nie wiruje na zebraniu pełnego OldGen ...

Aby to zrobić, trzeba użyć zaawansowanych opcji, takich jak :

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

Uwaga na te strefy i ich domyślna polityka alokacji zależy od używanego kolektora. Więc zmień jeden parametr na raz i sprawdź ponownie.

Jeśli cały ten ciąg powinien zawierać w pamięci wszystkie czasy na żywo JVM, dobrze jest dokonać internalizacji ich w PermGen o wystarczająco dużym rozmiarze z -XX:MaxPermSize i uniknąć zbierania w tej strefie dzięki -Xnoclassgc.

Zalecam włączenie tych opcji debugowania (nie oczekuje się narzutu) i ostatecznie opublikowanie dziennika GC, abyśmy mogli zorientować się w aktywności GC.

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

Patrzyłem na to i mógłbym spróbować. Dzieki za sugestie. –

Powiązane problemy