GNU/Linux >> Znalost Linux >  >> Linux

Java spotřebovává mnohem více paměti, než je velikost haldy (nebo správně velikost paměti Dockeru)

Virtuální paměť používaná procesem Java daleko přesahuje jen Java Heap. Víte, JVM obsahuje mnoho podsystémů:Garbage Collector, Class Loading, JIT kompilátory atd. a všechny tyto subsystémy vyžadují ke svému fungování určité množství RAM.

JVM není jediným spotřebitelem RAM. Nativní knihovny (včetně standardní knihovny tříd Java) mohou také alokovat nativní paměť. A to nebude viditelné ani pro Native Memory Tracking. Samotná Java aplikace může také využívat off-heap paměť pomocí přímých ByteBufferů.

Co tedy vyžaduje paměť v procesu Java?

Části JVM (většinou zobrazené pomocí Native Memory Tracking)

  1. Java Heap

Nejviditelnější část. Zde žijí objekty Java. Halda zabere až -Xmx množství paměti.

  1. Garbage Collector

Struktury a algoritmy GC vyžadují další paměť pro správu haldy. Těmito strukturami jsou Mark Bitmap, Mark Stack (pro procházení objektového grafu), Remembered Sets (pro záznam mezioblastních referencí) a další. Některé z nich jsou přímo laditelné, např. -XX:MarkStackSizeMax , jiné závisí na rozložení haldy, např. větší jsou oblasti G1 (-XX:G1HeapRegionSize ), menší jsou zapamatované sady.

Režie paměti GC se mezi algoritmy GC liší. -XX:+UseSerialGC a -XX:+UseShenandoahGC mít nejmenší režii. G1 nebo CMS mohou snadno využít přibližně 10 % celkové velikosti haldy.

  1. Cache kódu

Obsahuje dynamicky generovaný kód:JIT-kompilované metody, interpret a run-time pahýly. Jeho velikost je omezena -XX:ReservedCodeCacheSize (ve výchozím nastavení 240 milionů). Vypněte -XX:-TieredCompilation snížit množství zkompilovaného kódu a tím i využití mezipaměti kódu.

  1. Kompilátor

Samotný kompilátor JIT také vyžaduje paměť, aby vykonával svou práci. To lze opět snížit vypnutím vrstvené kompilace nebo snížením počtu vláken kompilátoru:-XX:CICompilerCount .

  1. Načítání třídy

Metadata tříd (bajtové kódy metody, symboly, fondy konstant, anotace atd.) jsou uložena v oblasti mimo haldu zvané Metaspace. Čím více tříd je načteno, tím více metaprostoru je použito. Celkové využití může být omezeno -XX:MaxMetaspaceSize (ve výchozím nastavení neomezeně) a -XX:CompressedClassSpaceSize (1G ve výchozím nastavení).

  1. Tabulky symbolů

Dvě hlavní hashovací tabulky JVM:tabulka Symbol obsahuje jména, podpisy, identifikátory atd. a tabulka String obsahuje odkazy na vložené řetězce. Pokud sledování Native Memory Tracking ukazuje značné využití paměti tabulkou řetězců, pravděpodobně to znamená, že aplikace nadměrně volá String.intern .

  1. Vlákna

Zásobníky vláken jsou také zodpovědné za odběr RAM. Velikost zásobníku je řízena -Xss . Výchozí hodnota je 1M na vlákno, ale naštěstí to není tak špatné. OS přiděluje stránky paměti líně, tj. při prvním použití, takže skutečné využití paměti bude mnohem nižší (obvykle 80-200 KB na zásobník vláken). Napsal jsem skript, abych odhadl, kolik z RSS patří do zásobníků vláken Java.

Existují další části JVM, které alokují nativní paměť, ale obvykle nehrají velkou roli v celkové spotřebě paměti.

Přímé vyrovnávací paměti

Aplikace může explicitně požadovat paměť mimo haldu voláním ByteBuffer.allocateDirect . Výchozí limit mimo haldu je roven -Xmx , ale lze jej přepsat pomocí -XX:MaxDirectMemorySize . Přímé ByteBuffery jsou součástí Other sekce výstupu NMT (nebo Internal před JDK 11).

Množství použité přímé paměti je viditelné přes JMX, např. v JConsole nebo Java Mission Control:

Kromě přímých ByteBufferů může existovat MappedByteBuffers - soubory mapované do virtuální paměti procesu. NMT je nesleduje, ale MappedByteBuffers mohou také zabírat fyzickou paměť. A neexistuje žádný jednoduchý způsob, jak omezit, kolik mohou vzít. Skutečné využití můžete vidět na mapě paměti procesu:pmap -x <pid>

Address           Kbytes    RSS    Dirty Mode  Mapping
...
00007f2b3e557000   39592   32956       0 r--s- some-file-17405-Index.db
00007f2b40c01000   39600   33092       0 r--s- some-file-17404-Index.db
                           ^^^^^               ^^^^^^^^^^^^^^^^^^^^^^^^

Nativní knihovny

Kód JNI načten System.loadLibrary může alokovat tolik paměti, kolik chce, bez kontroly ze strany JVM. Týká se to i standardní knihovny tříd Java. Zejména neuzavřené prostředky Java se mohou stát zdrojem úniku nativní paměti. Typickými příklady jsou ZipInputStream nebo DirectoryStream .

Agenti JVMTI, zejména jdwp ladicí agent – ​​může také způsobit nadměrnou spotřebu paměti.

Tato odpověď popisuje, jak profilovat alokace nativní paměti pomocí asynchronního profilu.

Problémy s alokátorem

Proces obvykle požaduje nativní paměť buď přímo z OS (podle mmap systémové volání) nebo pomocí malloc - standardní alokátor libc. Na druhé straně malloc požaduje velké kusy paměti z OS pomocí mmap a poté spravuje tyto bloky podle vlastního algoritmu přidělování. Problém je v tom, že tento algoritmus může vést k fragmentaci a nadměrnému využití virtuální paměti.

jemalloc , alternativní alokátor, se často zdá chytřejší než běžný libc malloc , takže přepněte na jemalloc může mít zdarma za následek menší stopu.

Závěr

Neexistuje žádný zaručený způsob, jak odhadnout využití plné paměti procesem Java, protože je třeba vzít v úvahu příliš mnoho faktorů.

Total memory = Heap + Code Cache + Metaspace + Symbol tables +
               Other JVM structures + Thread stacks +
               Direct buffers + Mapped files +
               Native Libraries + Malloc overhead + ...

Je možné zmenšit nebo omezit určité oblasti paměti (jako Code Cache) pomocí příznaků JVM, ale mnoho dalších je zcela mimo kontrolu JVM.

Jedním z možných přístupů k nastavení limitů Dockeru by bylo sledovat skutečné využití paměti v „normálním“ stavu procesu. Existují nástroje a techniky pro vyšetřování problémů se spotřebou paměti Java:Native Memory Tracking, pmap, jemalloc, async-profiler.

Aktualizovat

Zde je záznam mé prezentace Memory Footprint of a Java Process.

V tomto videu diskutuji o tom, co může spotřebovávat paměť v procesu Java, jak monitorovat a omezit velikost určitých oblastí paměti a jak profilovat úniky nativní paměti v aplikaci Java.


https://developers.redhat.com/blog/2017/04/04/openjdk-and-containers/:

Proč, když zadám -Xmx=1g, moje JVM spotřebuje více paměti než 1gbof paměti?

Zadání -Xmx=1g říká JVM, aby alokovalo 1GB haldu. Říká se, že JVM omezí využití celé paměti na 1 gb. Existují tabulky karet, mezipaměti kódu a všechny druhy jiných datových struktur mimo haldu. Parametr, který používáte k určení celkového využití paměti, je -XX:MaxRAM. Uvědomte si, že s -XX:MaxRam=500m bude vaše halda přibližně 250 MB.

Java vidí velikost paměti hostitele a není si vědoma žádných omezení paměti kontejneru. Nevytváří tlak na paměť, takže GC také nepotřebuje uvolnit použitou paměť. Doufám, že XX:MaxRAM vám pomůže snížit paměťovou stopu. Nakonec můžete vyladit konfiguraci GC (-XX:MinHeapFreeRatio ,-XX:MaxHeapFreeRatio , ...)

Existuje mnoho typů paměťových metrik. Zdá se, že Docker hlásí velikost paměti RSS, která se může lišit od "potvrzené" paměti hlášené jcmd (starší verze Dockeru hlásí RSS+mezipaměť jako využití paměti).Dobrá diskuze a odkazy:Rozdíl mezi velikostí rezidentní sady (RSS) a celkovou potvrzenou pamětí Java (NMT) pro JVM běžící v kontejneru Docker

(RSS) paměť mohou požírat i některé další utility v kontejneru - shell, správce procesů, ... Nevíme, co ještě v kontejneru běží a jak spouštíte procesy v kontejneru.


TL;DR

Podrobné využití paměti je zajištěno detaily NMT (Native Memory Tracking) (hlavně metadata kódu a garbage collector). Kromě toho kompilátor a optimalizátor Java C1/C2 spotřebovává paměť, která není uvedena v souhrnu.

Paměťovou stopu lze snížit pomocí příznaků JVM (ale má to dopady).

Velikost kontejneru Docker musí být provedena testováním s očekávaným zatížením aplikace.

Podrobnosti pro jednotlivé komponenty

Sdílený prostor třídy lze zakázat uvnitř kontejneru, protože třídy nebudou sdíleny jiným procesem JVM. Lze použít následující příznak. Odebere sdílený prostor třídy (17 MB).

-Xshare:off

Sběrač odpadu seriál má minimální paměťovou stopu za cenu delší pauzy během zpracování odpadu (viz srovnání Aleksey Shipilëva mezi GC na jednom obrázku). Lze jej povolit pomocí následujícího příznaku. Může ušetřit až použité místo GC (48 MB).

-XX:+UseSerialGC

Kompilátor C2 lze deaktivovat pomocí následujícího příznaku, aby se snížily profilovací údaje používané k rozhodování, zda metodu optimalizovat či nikoli.

-XX:+TieredCompilation -XX:TieredStopAtLevel=1

Kódový prostor je zmenšen o 20 MB. Kromě toho je paměť mimo JVM snížena o 80 MB (rozdíl mezi prostorem NMT a prostorem RSS). Optimalizační kompilátor C2 potřebuje 100 MB.

Kompilátory C1 a C2 lze deaktivovat pomocí následujícího příznaku.

-Xint

Paměť mimo JVM je nyní nižší než celkový potvrzený prostor. Kódový prostor je zmenšen o 43 MB. Pozor, má to zásadní vliv na výkon aplikace. Zakázání kompilátoru C1 a C2 sníží použitou paměť o 170 MB.

Pomocí kompilátoru Graal VM (náhrada C2) vede k trochu menšímu zatěžování paměti. Zvětší se o 20 MB místa v paměti kódu a zmenší se o 60 MB z vnější paměti JVM.

Článek Java Memory Management for JVM poskytuje některé relevantní informace o různých paměťových prostorech. Oracle poskytuje některé podrobnosti v dokumentaci Native Memory Tracking. Další podrobnosti o úrovni kompilace v pokročilých zásadách kompilace a v deaktivaci C2 snižují velikost mezipaměti kódu na faktor 5. Některé podrobnosti o tom, proč JVM hlásí více potvrzené paměti než velikost rezidentní sady procesu Linux? když jsou oba kompilátory zakázány.


Linux
  1. Virtuální počítač s 32bitovým OS CentOS 5 neřeší fyzickou paměť větší než 3 GB

  2. Limit paměti PHP

  3. CentOS / RHEL 6 :Jak omezit paměťové zdroje pro konkrétního uživatele pomocí cgroups

  1. Využití virtuální paměti z Javy pod Linuxem, příliš mnoho paměti

  2. Jak zkontrolovat velikost haldy pro proces v Linuxu

  3. jak zkontrolovat velikost haldy přidělenou pro jvm linuxem

  1. Máte nějaké zkušenosti s Javou na Raspberry PI?

  2. Existuje způsob, jak nastavit velikost seznamu historie v bash na více než 5000 řádků?

  3. Jak přidělit více místa pro výměnu a zvětšit jeho velikost větší než Ram?