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)
- Java Heap
Nejviditelnější část. Zde žijí objekty Java. Halda zabere až -Xmx
množství paměti.
- 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.
- 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.
- 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
.
- 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í).
- 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
.
- 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.