Java zużywa znacznie więcej pamięci niż rozmiar sterty (lub poprawnie rozmiar limitu pamięci Docker)

118

W przypadku mojej aplikacji pamięć używana przez proces Java jest znacznie większa niż wielkość sterty.

System, w którym działają kontenery, zaczyna mieć problem z pamięcią, ponieważ kontener zajmuje znacznie więcej pamięci niż rozmiar sterty.

Rozmiar sterty jest ustawiony na 128 MB ( -Xmx128m -Xms128m), podczas gdy kontener zajmuje do 1 GB pamięci. W normalnych warunkach potrzebuje 500 MB. Jeśli kontener dockera ma limit poniżej (np. mem_limit=mem_limit=400MB), Proces zostaje zabity przez zabójcę pamięci systemu operacyjnego.

Czy mógłbyś wyjaśnić, dlaczego proces Java zużywa znacznie więcej pamięci niż sterta? Jak poprawnie ustawić limit pamięci Dockera? Czy istnieje sposób na zmniejszenie ilości pamięci poza stertą, jaką zajmuje proces Java?


Zbieram szczegóły dotyczące problemu za pomocą polecenia z natywnego śledzenia pamięci w JVM .

Z systemu hosta pobieram pamięć używaną przez kontener.

$ docker stats --no-stream 9afcb62a26c8
CONTAINER ID        NAME                                                                                        CPU %               MEM USAGE / LIMIT   MEM %               NET I/O             BLOCK I/O           PIDS
9afcb62a26c8        xx-xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.0acbb46bb6fe3ae1b1c99aff3a6073bb7b7ecf85   0.93%               461MiB / 9.744GiB   4.62%               286MB / 7.92MB      157MB / 2.66GB      57

Z wnętrza pojemnika uzyskuję pamięć używaną przez proces.

$ ps -p 71 -o pcpu,rss,size,vsize
%CPU   RSS  SIZE    VSZ
11.2 486040 580860 3814600

$ jcmd 71 VM.native_memory
71:

Native Memory Tracking:

Total: reserved=1631932KB, committed=367400KB
-                 Java Heap (reserved=131072KB, committed=131072KB)
                            (mmap: reserved=131072KB, committed=131072KB) 

-                     Class (reserved=1120142KB, committed=79830KB)
                            (classes #15267)
                            (  instance classes #14230, array classes #1037)
                            (malloc=1934KB #32977) 
                            (mmap: reserved=1118208KB, committed=77896KB) 
                            (  Metadata:   )
                            (    reserved=69632KB, committed=68272KB)
                            (    used=66725KB)
                            (    free=1547KB)
                            (    waste=0KB =0.00%)
                            (  Class space:)
                            (    reserved=1048576KB, committed=9624KB)
                            (    used=8939KB)
                            (    free=685KB)
                            (    waste=0KB =0.00%)

-                    Thread (reserved=24786KB, committed=5294KB)
                            (thread #56)
                            (stack: reserved=24500KB, committed=5008KB)
                            (malloc=198KB #293) 
                            (arena=88KB #110)

-                      Code (reserved=250635KB, committed=45907KB)
                            (malloc=2947KB #13459) 
                            (mmap: reserved=247688KB, committed=42960KB) 

-                        GC (reserved=48091KB, committed=48091KB)
                            (malloc=10439KB #18634) 
                            (mmap: reserved=37652KB, committed=37652KB) 

-                  Compiler (reserved=358KB, committed=358KB)
                            (malloc=249KB #1450) 
                            (arena=109KB #5)

-                  Internal (reserved=1165KB, committed=1165KB)
                            (malloc=1125KB #3363) 
                            (mmap: reserved=40KB, committed=40KB) 

-                     Other (reserved=16696KB, committed=16696KB)
                            (malloc=16696KB #35) 

-                    Symbol (reserved=15277KB, committed=15277KB)
                            (malloc=13543KB #180850) 
                            (arena=1734KB #1)

-    Native Memory Tracking (reserved=4436KB, committed=4436KB)
                            (malloc=378KB #5359) 
                            (tracking overhead=4058KB)

-        Shared class space (reserved=17144KB, committed=17144KB)
                            (mmap: reserved=17144KB, committed=17144KB) 

-               Arena Chunk (reserved=1850KB, committed=1850KB)
                            (malloc=1850KB) 

-                   Logging (reserved=4KB, committed=4KB)
                            (malloc=4KB #179) 

-                 Arguments (reserved=19KB, committed=19KB)
                            (malloc=19KB #512) 

-                    Module (reserved=258KB, committed=258KB)
                            (malloc=258KB #2356) 

$ cat /proc/71/smaps | grep Rss | cut -d: -f2 | tr -d " " | cut -f1 -dk | sort -n | awk '{ sum += $1 } END { print sum }'
491080

Aplikacja jest serwerem WWW korzystającym z Jetty / Jersey / CDI w pakiecie o wielkości 36 MB.

Używane są następujące wersje systemu operacyjnego i Java (wewnątrz kontenera). Obraz platformy Docker jest oparty na platformie openjdk:11-jre-slim.

$ java -version
openjdk version "11" 2018-09-25
OpenJDK Runtime Environment (build 11+28-Debian-1)
OpenJDK 64-Bit Server VM (build 11+28-Debian-1, mixed mode, sharing)
$ uname -a
Linux service1 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2018 x86_64 GNU/Linux

https://gist.github.com/prasanthj/48e7063cac88eb396bc9961fb3149b58

Nicolas Henneaux
źródło
6
Sterta jest miejscem, w którym przydzielane są obiekty, jednak JVM ma wiele innych regionów pamięci, w tym biblioteki współdzielone, bezpośrednie bufory pamięci, stosy wątków, komponenty GUI, metaprzestrzeń. Musisz przyjrzeć się, jak duża może być JVM i ustawić limit na tyle wysoki, że wolisz, aby proces umarł, niż go więcej używać.
Peter Lawrey
2
Wygląda na to, że GC zużywa dużo pamięci. Zamiast tego możesz spróbować użyć kolektora CMS. Wygląda na to, że ~ 125 MB jest używane na kod metaspace +, jednak bez zmniejszania bazy kodu prawdopodobnie nie będziesz w stanie go zmniejszyć. Zaangażowana przestrzeń jest bliska twojego limitu, więc nic dziwnego, że zostaje zabita.
Peter Lawrey
gdzie / jak ustawiasz konfigurację -Xms i -Xmx?
Mick
1
Czy program wykonuje wiele operacji na plikach (np. Tworzy pliki o rozmiarze gigabajtów)? Jeśli tak, powinieneś wiedzieć, że cgroupsdodaje to pamięć podręczną dysku do używanej pamięci - nawet jeśli jest obsługiwana przez jądro i jest niewidoczna dla programu użytkownika. (Pamiętaj, polecenia psi docker statsnie licz pamięci podręcznej dysku.)
Lorinczy Zsigmond

Odpowiedzi:

205

Pamięć wirtualna używana przez proces Java wykracza daleko poza samą stertę języka Java. Wiesz, JVM zawiera wiele podsystemów: Garbage Collector, Class Loading, JIT compilers etc., a wszystkie te podsystemy wymagają do działania pewnej ilości pamięci RAM.

JVM nie jest jedynym konsumentem pamięci RAM. Biblioteki natywne (w tym standardowa biblioteka klas Java) mogą również przydzielać pamięć natywną. I nie będzie to nawet widoczne dla Native Memory Tracking. Sama aplikacja Java może również korzystać z pamięci poza stertą za pomocą bezpośrednich ByteBufferów.

Więc co zajmuje pamięć w procesie Java?

Części JVM (głównie pokazywane przez Native Memory Tracking)

  1. Java Heap

    Najbardziej oczywista część. Tutaj żyją obiekty Java. Sterta zajmuje za -Xmxdużo pamięci.

  2. Śmieciarz

    Struktury i algorytmy GC wymagają dodatkowej pamięci do zarządzania stertą. Struktury te to Mark Bitmap, Mark Stack (do przechodzenia przez wykres obiektów), Zapamiętane zestawy (do rejestrowania odniesień między regionami) i inne. Niektóre z nich są bezpośrednio przestrajalne, np. -XX:MarkStackSizeMaxInne zależą od układu sterty, np. Im większe są regiony G1 ( -XX:G1HeapRegionSize), tym mniejsze są zapamiętywane zbiory.

    Narzut pamięci GC różni się w zależności od algorytmu GC. -XX:+UseSerialGCi -XX:+UseShenandoahGCmają najmniejsze koszty ogólne. G1 lub CMS mogą z łatwością wykorzystać około 10% całkowitego rozmiaru sterty.

  3. Pamięć podręczna kodu

    Zawiera dynamicznie generowany kod: metody skompilowane w JIT, interpreter i kody pośredniczące czasu wykonywania. Jego rozmiar jest ograniczony -XX:ReservedCodeCacheSize(domyślnie 240M). Wyłącz, -XX:-TieredCompilationaby zmniejszyć ilość skompilowanego kodu, a tym samym użycie pamięci podręcznej kodu.

  4. Kompilator

    Sam kompilator JIT również wymaga pamięci do wykonania swojej pracy. Można to jeszcze zmniejszone poprzez wyłączenie Kompilacja warstwowych lub zmniejszenie liczby wątków kompilatora: -XX:CICompilerCount.

  5. Ładowanie klasy

    Metadane klas (kody bajtowe metod, symbole, pule stałych, adnotacje itp.) Są przechowywane w obszarze poza stertą zwanym Metaspace. Im więcej klas jest ładowanych - tym więcej metaprzestrzeni jest używanych. Całkowite wykorzystanie może być ograniczone przez -XX:MaxMetaspaceSize(domyślnie nieograniczone) i -XX:CompressedClassSpaceSize(domyślnie 1G).

  6. Tabele symboli

    Dwie główne tabele skrótów JVM: tabela Symbol zawiera nazwy, podpisy, identyfikatory itp., A tabela String zawiera odniesienia do wbudowanych ciągów. Jeśli Native Memory Tracking wskazuje na znaczne użycie pamięci przez tabelę String, prawdopodobnie oznacza to nadmierne wywołania aplikacji String.intern.

  7. Wątki

    Stosy wątków są również odpowiedzialne za pobieranie pamięci RAM. Rozmiar stosu jest kontrolowany przez -Xss. Wartość domyślna to 1 MB na wątek, ale na szczęście sytuacja nie jest taka zła. System operacyjny leniwie przydziela strony pamięci, tj. Przy pierwszym użyciu, więc rzeczywiste użycie pamięci będzie znacznie niższe (zwykle 80-200 KB na stos wątków). Napisałem skrypt, aby oszacować, ile RSS należy do stosów wątków Java.

    Istnieją inne części maszyny JVM, które przydzielają pamięć natywną, ale zazwyczaj nie odgrywają one dużej roli w całkowitym zużyciu pamięci.

Bufory bezpośrednie

Aplikacja może jawnie zażądać pamięci poza stertą, wywołując ByteBuffer.allocateDirect. Domyślny limit poza stertą jest równy -Xmx, ale można go zastąpić -XX:MaxDirectMemorySize. Direct ByteBuffers są zawarte w Othersekcji wyjścia NMT (lub Internalprzed JDK 11).

Ilość wykorzystanej pamięci bezpośredniej jest widoczna poprzez JMX, np. W JConsole lub Java Mission Control:

BufferPool MBean

Oprócz bezpośrednich ByteBufferów mogą istnieć MappedByteBuffers- pliki mapowane na pamięć wirtualną procesu. NMT ich nie śledzi, jednak MappedByteBuffers może również zajmować pamięć fizyczną. I nie ma prostego sposobu na ograniczenie tego, ile mogą znieść. Możesz po prostu zobaczyć rzeczywiste zużycie, patrząc na mapę pamięci 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
                           ^^^^^               ^^^^^^^^^^^^^^^^^^^^^^^^

Biblioteki rodzime

Kod JNI załadowany przez System.loadLibrarymoże przydzielić tyle pamięci poza stertą, ile chce, bez kontroli ze strony JVM. Dotyczy to również standardowej biblioteki klas Java. W szczególności niezamknięte zasoby Java mogą stać się źródłem natywnego wycieku pamięci. Typowe przykłady to ZipInputStreamlub DirectoryStream.

Agenty JVMTI, w szczególności jdwpagent debugujący - mogą również powodować nadmierne zużycie pamięci.

W tej odpowiedzi opisano, jak profilować natywne alokacje pamięci za pomocą programu Async-Profiler .

Problemy z alokatorem

Proces zazwyczaj żąda pamięci natywnej bezpośrednio z systemu operacyjnego (przez mmapwywołanie systemowe) lub przy użyciu malloc- standardowego alokatora libc. Z kolei mallocżąda dużych porcji pamięci z systemu operacyjnego przy użyciu mmap, a następnie zarządza tymi fragmentami zgodnie z własnym algorytmem alokacji. Problem w tym, że ten algorytm może prowadzić do fragmentacji i nadmiernego wykorzystania pamięci wirtualnej .

jemalloc, alternatywny alokator, często wydaje się mądrzejszy niż zwykłe libc malloc, więc przejście na jemallocmoże zaowocować mniejszą ilością miejsca za darmo.

Wniosek

Nie ma gwarantowanego sposobu oszacowania pełnego wykorzystania pamięci przez proces Java, ponieważ należy wziąć pod uwagę zbyt wiele czynników.

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

Możliwe jest zmniejszenie lub ograniczenie niektórych obszarów pamięci (takich jak Code Cache) za pomocą flag JVM, ale wiele innych jest w ogóle poza kontrolą JVM.

Jednym z możliwych sposobów ustawiania limitów Dockera byłoby obserwowanie rzeczywistego wykorzystania pamięci w „normalnym” stanie procesu. Istnieją narzędzia i techniki umożliwiające badanie problemów z wykorzystaniem pamięci Java: natywne śledzenie pamięci , pmap , jemalloc , async-profiler .

Aktualizacja

Oto nagranie mojej prezentacji Ślad pamięciowy procesu Java .

W tym filmie omawiam, co może zużywać pamięć w procesie Java, jak monitorować i ograniczać rozmiar niektórych obszarów pamięci oraz jak profilować natywne wycieki pamięci w aplikacji Java.

apangin
źródło
1
Czy nie są internowane ciągi znaków w stercie od czasu jdk7? ( bugs.java.com/bugdatabase/view_bug.do?bug_id=6962931 ) - może się mylę.
j-keck
5
@ j-keck Obiekty typu string znajdują się w stercie, ale tablica haszująca (zasobniki i wpisy z odniesieniami i kodami skrótu) znajduje się poza stertą. Przeredagowałem zdanie, aby było bardziej precyzyjne. Dzięki za wskazanie.
apangin
aby to dodać, nawet jeśli używasz niebezpośrednich buforów ByteBuffers, maszyna JVM przydzieli tymczasowe bufory bezpośrednie w pamięci natywnej bez narzuconych ograniczeń pamięci. Por. evanjones.ca/java-bytebuffer-leak.html
Cpt. Senkfuss
16

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

Dlaczego jest tak, że podam -Xmx = 1g, moja maszyna JVM zużywa więcej pamięci niż 1 GB pamięci?

Określenie -Xmx = 1g oznacza dla maszyny JVM przydzielenie sterty 1 GB. Nie mówi JVM, aby ograniczył całe zużycie pamięci do 1 GB. Istnieją tabele kart, pamięci podręczne kodu i wszelkiego rodzaju inne struktury danych poza stertą. Parametr używany do określenia całkowitego użycia pamięci to -XX: MaxRAM. Należy pamiętać, że przy -XX: MaxRam = 500m twoja sterta będzie miała około 250mb.

Java widzi rozmiar pamięci hosta i nie jest świadoma żadnych ograniczeń pamięci kontenera. Nie powoduje presji pamięci, więc GC również nie musi zwalniać używanej pamięci. Mam nadzieję, XX:MaxRAMże pomoże ci zmniejszyć zużycie pamięci. Ostatecznie, można dostosować konfigurację GC ( -XX:MinHeapFreeRatio, -XX:MaxHeapFreeRatio...)


Istnieje wiele rodzajów metryk pamięci. Wydaje się, że Docker zgłasza rozmiar pamięci RSS, który może być inny niż „zatwierdzona” pamięć zgłaszana przez jcmd(starsze wersje raportu Docker RSS + cache jako użycie pamięci). Dobra dyskusja i linki: różnica między Resident Set Size (RSS) a całkowitą zatwierdzoną pamięcią Java (NMT) dla maszyny JVM działającej w kontenerze Docker

(RSS) pamięć może być zjadana również przez inne narzędzia w kontenerze - powłoka, menedżer procesów, ... Nie wiemy, co jeszcze działa w kontenerze i jak uruchamiasz procesy w kontenerze.

Jan Garaj
źródło
Rzeczywiście jest lepiej z -XX:MaxRam. Myślę, że nadal używa więcej niż zdefiniowane maksimum, ale jest lepiej, dzięki!
Nicolas Henneaux
Może naprawdę potrzebujesz więcej pamięci dla tej instancji Java. Istnieje 15267 klas, 56 wątków.
Jan Garaj
1
Oto więcej szczegółów, argumenty Java -Xmx128m -Xms128m -Xss228k -XX:MaxRAM=256m -XX:+UseSerialGC, dane produkcyjne Docker 428.5MiB / 600MiBi jcmd 58 VM.native_memory -> Native Memory Tracking: Total: reserved=1571296KB, committed=314316KB. JVM zajmuje około 300 MB, podczas gdy kontener potrzebuje 430 MB. Gdzie jest 130 MB między raportowaniem JVM a raportowaniem systemu operacyjnego?
Nicolas Henneaux
1
Dodano informację / link o pamięci RSS.
Jan Garaj
Dostarczony RSS pochodzi z kontenera dla procesu Java tylko ps -p 71 -o pcpu,rss,size,vsizez procesem Java mającym pid 71. Właściwie -XX:MaxRamto nie pomogło, ale podany link pomaga w szeregowym GC.
Nicolas Henneaux
8

TL; DR

Szczegółowe wykorzystanie pamięci jest zapewniane przez szczegóły natywnego śledzenia pamięci (NMT) (głównie metadane kodu i moduł odśmiecania pamięci). Oprócz tego kompilator Java i optymalizator C1 / C2 zużywają pamięć, która nie została zgłoszona w podsumowaniu.

Wykorzystanie pamięci można zmniejszyć za pomocą flag JVM (ale ma to wpływ).

Określanie rozmiaru kontenera platformy Docker należy przeprowadzić, testując aplikację z oczekiwanym obciążeniem.


Szczegóły dla każdego komponentu

Wspólna przestrzeń klasy można wyłączyć wewnątrz kontenera od zajęcia nie będą udostępniane przez inny proces JVM. Można użyć następującej flagi. Spowoduje to usunięcie współdzielonej przestrzeni klas (17 MB).

-Xshare:off

Śmieciarza seryjny ma minimalne zużycie pamięci kosztem dłuższego czasu wstrzymać garbage collect przetwarzania (patrz porównanie Aleksey Shipilëv między GC w jeden obraz ). Można go włączyć za pomocą następującej flagi. Może zaoszczędzić do używanego miejsca GC (48 MB).

-XX:+UseSerialGC

Kompilator C2 mogą być wyłączone z następującym flagą zmniejszyć profilowania dane wykorzystane do ustalenia, czy w celu optymalizacji lub nie metody.

-XX:+TieredCompilation -XX:TieredStopAtLevel=1

Przestrzeń kodowa została zmniejszona o 20 MB. Ponadto pamięć poza maszyną JVM jest zmniejszona o 80 MB (różnica między przestrzenią NMT a przestrzenią RSS). Optymalizujący kompilator C2 potrzebuje 100 MB.

W C1 i C2 kompilatory mogą być wyłączone z następującą flagą.

-Xint

Pamięć poza maszyną JVM jest teraz mniejsza niż całkowite zatwierdzone miejsce. Przestrzeń kodowa została zmniejszona o 43 MB. Uważaj, ma to duży wpływ na wydajność aplikacji. Wyłączenie kompilatora C1 i C2 zmniejsza ilość używanej pamięci o 170 MB.

Użycie kompilatora Graal VM (zastąpienie C2) prowadzi do nieco mniejszego zużycia pamięci. Zwiększa o 20 MB miejsce w pamięci kodu i zmniejsza o 60 MB poza pamięcią JVM.

Artykuł Java Memory Management for JVM zawiera kilka istotnych informacji na temat różnych przestrzeni pamięci. Oracle podaje pewne szczegóły w dokumentacji Native Memory Tracking . Więcej szczegółów na temat poziomu kompilacji w zaawansowanych zasadach kompilacji i wyłączeniu C2 zmniejsza rozmiar pamięci podręcznej kodu o współczynnik 5 . Niektóre szczegóły na temat Dlaczego JVM zgłasza więcej zaangażowanej pamięci niż rozmiar zestawu rezydentnego procesu Linux? gdy oba kompilatory są wyłączone.

Nicolas Henneaux
źródło
-1

Java potrzebuje dużo pamięci. Sama JVM potrzebuje dużo pamięci do działania. Sterta to pamięć, która jest dostępna wewnątrz maszyny wirtualnej, dostępna dla Twojej aplikacji. Ponieważ JVM to duży pakiet pełen wszystkich możliwych dodatków, samo załadowanie zajmuje dużo pamięci.

Zaczynając od Java 9, masz coś, co nazywa się Jigsaw projektu , co może zmniejszyć ilość pamięci używanej podczas uruchamiania aplikacji Java (wraz z czasem rozpoczęcia). Układanka projektu i nowy system modułów niekoniecznie zostały stworzone w celu zmniejszenia niezbędnej pamięci, ale jeśli to ważne, możesz spróbować.

Możesz spojrzeć na ten przykład: https://steveperkins.com/using-java-9-modularization-to-ship-zero-dependency-native-apps/ . Dzięki zastosowaniu systemu modułów zaowocowało to aplikacją CLI o wielkości 21MB (z wbudowanym JRE). JRE zajmuje ponad 200 MB. Powinno to przełożyć się na mniej alokowaną pamięć, gdy aplikacja jest uruchomiona (wiele nieużywanych klas JRE nie będzie już ładowanych).

Oto kolejny fajny samouczek: https://www.baeldung.com/project-jigsaw-java-modularity

Jeśli nie chcesz spędzać z tym czasu, możesz po prostu przydzielić więcej pamięci. Czasami to jest najlepsze.

adiian
źródło
Używanie jlinkjest dość restrykcyjne, ponieważ wymagało modułowości aplikacji. Moduł automatyczny nie jest obsługiwany, więc nie ma łatwego sposobu, aby się tam dostać.
Nicolas Henneaux
-1

Jak poprawnie ustawić limit pamięci Dockera? Sprawdź aplikację, monitorując ją przez jakiś czas. Aby ograniczyć pamięć kontenera, spróbuj użyć opcji -m, --memory bytes dla polecenia docker run - lub czegoś równoważnego, jeśli używasz go w inny sposób, np.

docker run -d --name my-container --memory 500m <iamge-name>

nie umie odpowiedzieć na inne pytania.

v_sukt
źródło