To byla dlouhodobá stížnost na Javu, ale je to z velké části bezvýznamné a obvykle založené na pohledu na špatné informace. Obvyklá fráze je něco jako "Hello World na Javě zabere 10 megabajtů! Proč to potřebuje?" No, tady je způsob, jak dosáhnout toho, aby Hello World na 64bitovém JVM tvrdil, že zabírá více než 4 gigabajty ... alespoň jednou formou měření.
java -Xms1024m -Xmx4096m com.example.Hello
Různé způsoby měření paměti
V Linuxu vám příkaz top poskytuje několik různých čísel paměti. Zde je to, co říká o příkladu Hello World:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 2120 kgregory 20 0 4373m 15m 7152 S 0 0.2 0:00.10 java
- VIRT je virtuální paměťový prostor:součet všeho na mapě virtuální paměti (viz níže). Je to z velké části bezvýznamné, kromě případů, kdy tomu tak není (viz níže).
- RES je velikost rezidentní sady:počet stránek, které jsou aktuálně umístěny v paměti RAM. Téměř ve všech případech je to jediné číslo, které byste měli použít, když řeknete „příliš velké“. Ale stále to není příliš dobré číslo, zvláště když mluvíme o Javě.
- SHR je množství rezidentní paměti, která je sdílena s jinými procesy. U procesu Java je to obvykle omezeno na sdílené knihovny a soubory JAR s mapováním paměti. V tomto příkladu jsem měl spuštěný pouze jeden proces Java, takže mám podezření, že 7k je výsledkem knihoven používaných OS.
- SWAP není ve výchozím nastavení zapnutý a zde se nezobrazuje. Označuje množství virtuální paměti, která je aktuálně rezidentní na disku, zda je či není skutečně ve swapovacím prostoru . Operační systém umí velmi dobře udržovat aktivní stránky v paměti RAM a jediným řešením pro výměnu je (1) nákup více paměti nebo (2) snížení počtu procesů, takže je nejlepší toto číslo ignorovat.
U Správce úloh systému Windows je situace o něco složitější. Ve Windows XP jsou sloupce "Využití paměti" a "Velikost virtuální paměti", ale oficiální dokumentace mlčí o tom, co znamenají. Windows Vista a Windows 7 přidávají další sloupce a jsou skutečně zdokumentovány. Z nich je nejužitečnější měření "Working Set"; zhruba odpovídá součtu RES a SHR na Linuxu.
Porozumění mapě virtuální paměti
Virtuální paměť spotřebovaná procesem je součet všeho, co je na mapě paměti procesu. To zahrnuje data (např. haldu Java), ale také všechny sdílené knihovny a soubory mapované v paměti používané programem. V Linuxu můžete použít příkaz pmap k zobrazení všech věcí namapovaných do prostoru procesu (od této chvíle budu odkazovat pouze na Linux, protože to je to, co používám; jsem si jistý, že existují ekvivalentní nástroje pro Okna). Zde je výňatek z paměťové mapy programu „Ahoj světe“; celá paměťová mapa má více než 100 řádků a není neobvyklé mít tisícřádkový seznam.
0000000040000000 36K r-x-- /usr/local/java/jdk-1.6-x64/bin/java 0000000040108000 8K rwx-- /usr/local/java/jdk-1.6-x64/bin/java 0000000040eba000 676K rwx-- [ anon ] 00000006fae00000 21248K rwx-- [ anon ] 00000006fc2c0000 62720K rwx-- [ anon ] 0000000700000000 699072K rwx-- [ anon ] 000000072aab0000 2097152K rwx-- [ anon ] 00000007aaab0000 349504K rwx-- [ anon ] 00000007c0000000 1048576K rwx-- [ anon ] ... 00007fa1ed00d000 1652K r-xs- /usr/local/java/jdk-1.6-x64/jre/lib/rt.jar ... 00007fa1ed1d3000 1024K rwx-- [ anon ] 00007fa1ed2d3000 4K ----- [ anon ] 00007fa1ed2d4000 1024K rwx-- [ anon ] 00007fa1ed3d4000 4K ----- [ anon ] ... 00007fa1f20d3000 164K r-x-- /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so 00007fa1f20fc000 1020K ----- /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so 00007fa1f21fb000 28K rwx-- /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so ... 00007fa1f34aa000 1576K r-x-- /lib/x86_64-linux-gnu/libc-2.13.so 00007fa1f3634000 2044K ----- /lib/x86_64-linux-gnu/libc-2.13.so 00007fa1f3833000 16K r-x-- /lib/x86_64-linux-gnu/libc-2.13.so 00007fa1f3837000 4K rwx-- /lib/x86_64-linux-gnu/libc-2.13.so ...
Rychlé vysvětlení formátu:každý řádek začíná adresou virtuální paměti segmentu. Následuje velikost segmentu, oprávnění a zdroj segmentu. Tato poslední položka je buď soubor nebo "anon", což označuje blok paměti alokovaný pomocí mmap.
Počínaje shora, máme
- Zavaděč JVM (tj. program, který se spustí, když zadáte
java
). To je velmi malé; vše, co dělá, je načtení do sdílených knihoven, kde je uložen skutečný kód JVM. - Spousta neaktivních bloků obsahujících haldu Java a interní data. Toto je Sun JVM, takže halda je rozdělena do několika generací, z nichž každá je vlastním paměťovým blokem. Všimněte si, že JVM přiděluje virtuální paměťový prostor na základě
-Xmx
hodnota; to mu umožňuje mít souvislou hromadu.-Xms
hodnota se interně používá k vyjádření toho, jak velká část haldy je „používána“, když se program spustí, a ke spuštění shromažďování odpadků, když se tento limit blíží. - Paměťově mapovaný soubor JAR, v tomto případě soubor, který obsahuje "třídy JDK." Když mapujete paměť JAR, můžete přistupovat k souborům v něm velmi efektivně (oproti tomu, abyste jej pokaždé četli od začátku). Sun JVM bude mapovat paměť všech JAR na cestě třídy; pokud kód vaší aplikace potřebuje přístup k JAR, můžete jej také zmapovat v paměti.
- Data na vlákno pro dvě vlákna. Blok 1M je zásobník závitů. Neměl jsem dobré vysvětlení pro 4k blok, ale @ericsoe ho identifikoval jako „ochranný blok“:nemá oprávnění ke čtení/zápisu, takže při přístupu způsobí chybu segmentu a JVM to zachytí a přeloží to na
StackOverFlowError
. U skutečné aplikace uvidíte desítky, ne-li stovky těchto záznamů, které se opakují na mapě paměti. - Jedna ze sdílených knihoven, která obsahuje skutečný kód JVM. Je jich několik.
- Sdílená knihovna pro standardní knihovnu C. Toto je jen jedna z mnoha věcí, které JVM načítá a které nejsou striktně součástí Javy.
Sdílené knihovny jsou obzvláště zajímavé:každá sdílená knihovna má alespoň dva segmenty:segment pouze pro čtení obsahující kód knihovny a segment pro čtení a zápis, který obsahuje globální data o jednotlivých procesech pro knihovnu (nevím, co segment bez oprávnění je; viděl jsem to pouze na x64 Linuxu). Část knihovny pouze pro čtení lze sdílet mezi všemi procesy, které knihovnu používají; například libc
má 1,5 milionu virtuální paměti, kterou lze sdílet.
Kdy je velikost virtuální paměti důležitá?
Mapa virtuální paměti obsahuje spoustu věcí. Některé z nich jsou pouze pro čtení, některé jsou sdílené a některé jsou přiděleny, ale nikdy se jich nedotknete (např. téměř všechny 4Gb haldy v tomto příkladu). Operační systém je ale dostatečně chytrý, aby nahrál pouze to, co potřebuje, takže velikost virtuální paměti je do značné míry irelevantní.
Velikost virtuální paměti je důležitá, pokud používáte 32bitový operační systém, kde můžete alokovat pouze 2Gb (nebo v některých případech 3Gb) adresového prostoru procesu. V takovém případě máte co do činění s nedostatečným zdrojem a možná budete muset udělat kompromisy, jako je zmenšení velikosti haldy, abyste mohli mapovat paměť velkého souboru nebo vytvořit spoustu vláken.
Ale vzhledem k tomu, že 64bitové stroje jsou všudypřítomné, nemyslím si, že bude trvat dlouho, než bude velikost virtuální paměti zcela irelevantní statistikou.
Kdy je důležitá velikost rezidentní sady?
Velikost rezidentní sady je ta část prostoru virtuální paměti, která je ve skutečnosti v RAM. Pokud vaše RSS naroste na významnou část vaší celkové fyzické paměti, možná je čas začít se znepokojovat. Pokud vaše RSS naroste tak, že zabere veškerou vaši fyzickou paměť a váš systém se začne vyměňovat, je čas začít si dělat starosti.
RSS je ale také zavádějící, zvláště na málo zatíženém stroji. Operační systém nevynakládá velké úsilí na znovuzískání stránek používaných procesem. Získáte tím jen malou výhodu a potenciál drahé chyby stránky, pokud se proces dotkne stránky v budoucnu. V důsledku toho může statistika RSS zahrnovat mnoho stránek, které se aktivně nepoužívají.
Sečteno a podtrženo
Pokud neprovádíte swapování, nedělejte si přehnané starosti s tím, co vám různé statistiky paměti říkají. S upozorněním, že stále rostoucí RSS může naznačovat nějaký druh úniku paměti.
U Java programu je mnohem důležitější věnovat pozornost tomu, co se děje v hromadě. Celkové množství spotřebovaného prostoru je důležité a existuje několik kroků, které můžete podniknout, abyste jej snížili. Důležitější je množství času, který strávíte sběrem odpadu, a které části hromady se shromažďují.
Přístup k disku (tj. databázi) je drahý a paměť je levná. Pokud můžete vyměnit jeden za druhý, udělejte to.
Množství paměti přidělené pro proces Java je téměř na stejné úrovni, jako bych očekával. Měl jsem podobné problémy se spuštěním Javy na vestavěných systémech s omezenou pamětí. Spuštěné jakékoli aplikace s libovolnými limity virtuálních počítačů nebo na systémech, které nemají dostatečné množství swapu, mají tendenci se zlomit. Zdá se, že je to povaha mnoha moderních aplikací, které nejsou navrženy pro použití v systémech s omezenými zdroji.
Máte několik dalších možností, které můžete vyzkoušet a omezit paměťovou stopu vašeho JVM. To může snížit nároky na virtuální paměť:
-XX:ReservedCodeCacheSize=32m Velikost rezervované mezipaměti kódu (v bajtech) - maximální velikost mezipaměti kódu. [Solaris 64-bit, amd64 a -server x86:48 m; v 1.5.0_06 a dřívějších, Solaris 64-bit a and64:1024m.]
-XX:MaxPermSize=64m Velikost Stálé generace. [5.0 a novější:64bitové virtuální počítače jsou zmenšeny o 30 % větší; 1,4amd64:96m; 1.3.1 -klient:32 m.]
Také byste měli nastavit –Xmx (maximální velikost haldy) na hodnotu co nejbližší skutečnému špičkovému využití paměti vaší aplikace. Domnívám se, že výchozí chování JVM je stále dvojnásobné velikost haldy pokaždé, když ji rozšíří na max. Pokud začnete s haldou 32 milionů a vaše aplikace dosáhla vrcholu na 65 milionů, pak by halda nakonec vzrostla na 32 milionů -> 64 milionů -> 128 milionů.
Můžete to také zkusit, aby byl VM méně agresivní při rozšiřování haldy:
-XX:MinHeapFreeRatio=40 Minimální procento volné haldy po GC, aby se zabránilo expanzi.
Také z toho, co si pamatuji z experimentování s tímto před několika lety, měl počet načtených nativních knihoven obrovský dopad na minimální stopu. Načítání java.net.Socket přidalo více než 15 milionů, pokud si dobře vzpomínám (a pravděpodobně ne).
Existuje známý problém s Java a glibc>=2.10 (zahrnuje Ubuntu>=10.04, RHEL>=6).
Lékem je nastavit toto prostředí. proměnná:
export MALLOC_ARENA_MAX=4
Pokud používáte Tomcat, můžete to přidat do TOMCAT_HOME/bin/setenv.sh
soubor.
Pro Docker toto přidejte do Dockerfile
ENV MALLOC_ARENA_MAX=4
Existuje článek IBM o nastavení MALLOC_ARENA_MAX https://www.ibm.com/developerworks/community/blogs/kevgrig/entry/linux_glibc_2_10_rhel_6_malloc_may_show_excessive_virtual_memory_usage?lang=en
Tento blogový příspěvek říká
Je známo, že rezidentní paměť se plíží způsobem podobným úniku paměti nebo fragmentaci paměti.
Existuje také otevřená chyba JDK JDK-8193521 „glibc plýtvá pamětí s výchozí konfigurací“
vyhledejte MALLOC_ARENA_MAX na Googlu nebo SO pro další reference.
Možná budete chtít vyladit také další možnosti malloc pro optimalizaci pro nízkou fragmentaci alokované paměti:
# tune glibc memory allocation, optimize for low fragmentation
# limit the number of arenas
export MALLOC_ARENA_MAX=2
# disable dynamic mmap threshold, see M_MMAP_THRESHOLD in "man mallopt"
export MALLOC_MMAP_THRESHOLD_=131072
export MALLOC_TRIM_THRESHOLD_=131072
export MALLOC_TOP_PAD_=131072
export MALLOC_MMAP_MAX_=65536