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
cgroups
dodaje 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, poleceniaps
idocker stats
nie licz pamięci podręcznej dysku.)Odpowiedzi:
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)
Java Heap
Najbardziej oczywista część. Tutaj żyją obiekty Java. Sterta zajmuje za
-Xmx
dużo pamięci.Ś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:MarkStackSizeMax
Inne 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:+UseSerialGC
i-XX:+UseShenandoahGC
mają najmniejsze koszty ogólne. G1 lub CMS mogą z łatwością wykorzystać około 10% całkowitego rozmiaru sterty.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:-TieredCompilation
aby zmniejszyć ilość skompilowanego kodu, a tym samym użycie pamięci podręcznej kodu.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
.Ł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).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
.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 wOther
sekcji wyjścia NMT (lubInternal
przed JDK 11).Ilość wykorzystanej pamięci bezpośredniej jest widoczna poprzez JMX, np. W JConsole lub Java Mission Control:
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>
Biblioteki rodzime
Kod JNI załadowany przez
System.loadLibrary
moż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 toZipInputStream
lubDirectoryStream
.Agenty JVMTI, w szczególności
jdwp
agent 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
mmap
wywołanie systemowe) lub przy użyciumalloc
- standardowego alokatora libc. Z koleimalloc
żąda dużych porcji pamięci z systemu operacyjnego przy użyciummap
, 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 libcmalloc
, więc przejście najemalloc
moż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.
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.
źródło
https://developers.redhat.com/blog/2017/04/04/openjdk-and-containers/ :
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.
źródło
-XX:MaxRam
. Myślę, że nadal używa więcej niż zdefiniowane maksimum, ale jest lepiej, dzięki!-Xmx128m -Xms128m -Xss228k -XX:MaxRAM=256m -XX:+UseSerialGC
, dane produkcyjneDocker 428.5MiB / 600MiB
ijcmd 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?ps -p 71 -o pcpu,rss,size,vsize
z procesem Java mającym pid 71. Właściwie-XX:MaxRam
to nie pomogło, ale podany link pomaga w szeregowym GC.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).
Ś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).
Kompilator C2 mogą być wyłączone z następującym flagą zmniejszyć profilowania dane wykorzystane do ustalenia, czy w celu optymalizacji lub nie metody.
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ą.
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.
źródło
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.
źródło
jlink
jest 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ć.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.
nie umie odpowiedzieć na inne pytania.
źródło