Java zużywająca znacznie więcej pamięci niż rozmiar sterty (lub rozmiar poprawnie dokowanego limitu pamięci)

Dla mojej aplikacji pamięć używana przez proces Java jest znacznie większa niż rozmiar 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 ograniczenie poniżej (np. mem_limit=mem_limit=400MB), proces zostaje zabity przez out of zabójca pamięci systemu operacyjnego.

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


Zbieram kilka szczegółów na temat problemu używając komendy z Native memory tracking w JVM.

Z systemu hosta otrzymuję 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, dostaję pamięć używana 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 internetowym używającym Jetty / Jersey / CDI w pakiecie fat o długości 36 MB.

Używana jest następująca wersja OS i Java (wewnątrz kontenera). Obraz dokera oparty jest na 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

Author: Nicolas Henneaux, 2018-11-23

5 answers

[32]}Pamięć wirtualna używana przez proces Java wykracza daleko poza samą stertę Javy. JVM zawiera wiele podsytemów: Garbage Collector, Class Loading, JIT compilers itp., a wszystkie te podsystemy wymagają pewnej ilości pamięci RAM do działania.

JVM nie jest jedynym konsumentem pamięci RAM. Biblioteki natywne (w tym standardowa biblioteka klas Java) mogą również przydzielać pamięć natywną. A to nie będzie nawet widoczne dla natywnego śledzenia pamięci. Sama aplikacja Java może również korzystać z pamięci off-heap przez sposoby bezpośredniego bufora bajtowego.

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

Część JVM (najczęściej wyświetlana przez natywne śledzenie pamięci)]}
  1. Java Heap

Najbardziej oczywistą częścią. Tutaj żyją Obiekty Javy. Sterta zajmuje do -Xmx ilość pamięci.

  1. Garbage Collector

Struktury i algorytmy GC wymagają dodatkowej pamięci do zarządzania stertą. Struktury te to Mark Bitmap, Mark Stack (do przechodzenia grafu obiektowego), Zapamiętane Zestawy (do zapisu odniesień międzyregionalnych) i inne. Niektóre z nich są bezpośrednio przestrajalne, np. -XX:MarkStackSizeMax, inne zależą od układu sterty, np. większe są regionami G1 (-XX:G1HeapRegionSize), mniejsze są zapamiętanymi zbiorami.

Nadmiarowość pamięci GC różni się w zależności od algorytmów GC. -XX:+UseSerialGC i -XX:+UseShenandoahGC mają najmniejsze nad głową. G1 lub CMS mogą z łatwością wykorzystywać około 10% całkowitego rozmiaru stosu.

  1. Code Cache

Zawiera dynamicznie generowany kod: JIT-metody skompilowane, interpreter i stubów. Jego rozmiar jest ograniczony przez -XX:ReservedCodeCacheSize (domyślnie 240M). Wyłącz -XX:-TieredCompilation, aby zmniejszyć ilość skompilowanego kodu, a tym samym użycie pamięci podręcznej kodu.

  1. kompilator

Sam kompilator JIT również wymaga pamięci do wykonywania swojej pracy. Można to ponownie zmniejszyć poprzez wyłączenie kompilacji warstwowej lub poprzez zmniejszenie liczby wątków kompilatora: -XX:CICompilerCount.

  1. Class loading

Metadane klasy (bajty metod, symbole, stałe pule, adnotacje itd.) jest przechowywany w obszarze off-heap o nazwie Metaspace. Im więcej klas jest ładowanych - tym więcej metaspace jest używane. Całkowite użycie może być ograniczone przez -XX:MaxMetaspaceSize (domyślnie nieograniczone) i -XX:CompressedClassSpaceSize (domyślnie 1G).

  1. Tablice symboli

Dwa główne Hashtable JVM: tabela symboli zawiera nazwy, podpisy, identyfikatory itp. a tabela łańcuchów zawiera odniesienia do internowanych łańcuchów. Jeśli natywne śledzenie pamięci wskazuje na znaczące wykorzystanie pamięci przez tabelę łańcuchową, prawdopodobnie oznacza, że aplikacja nadmiernie wywołuje String.intern.

  1. wątki

Stosy wątków są również odpowiedzialne za pobieranie pamięci RAM. Wielkość stosu jest kontrolowana przez -Xss. Domyślnie jest 1M na wątek, ale na szczęście rzeczy nie są takie złe. OS przydziela strony pamięci leniwie, tzn. przy pierwszym użyciu, więc rzeczywiste zużycie pamięci będzie znacznie niższe (zazwyczaj 80-200 KB na stos wątków). Napisałem skrypt aby oszacować ile RSS należy do wątku Java stosy.

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

Bufory bezpośrednie

Aplikacja może jawnie zażądać pamięci off-heap przez wywołanie ByteBuffer.allocateDirect. Domyślny limit off-heap jest równy -Xmx, ale może być nadpisany przez -XX:MaxDirectMemorySize. Bezpośrednie bufory bajtów są zawarte w Other sekcji wyjścia NMT (lub Internal przed JDK 11).

Ilość użytej pamięci bezpośredniej jest widoczna przez JMX, np. w JConsole lub Java Mission Control:

BufferPool MBean

Oprócz bezpośrednich buforów bajtowych mogą być MappedByteBuffers - pliki mapowane do pamięci wirtualnej procesu. NMT ich nie śledzi, jednak Mappedbytebuffery mogą również pobierać pamięć fizyczną. I nie ma prostego sposobu, aby ograniczyć, ile mogą wziąć. Można po prostu zobaczyć rzeczywiste uż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 natywne

Kod JNI załadowany przez {[21] } może przydzielić jako dużo pamięci poza stertą, jak chce, bez kontroli od strony JVM. Dotyczy to również standardowej biblioteki klas Java. W szczególności niezamknięte zasoby Javy mogą stać się źródłem natywnego wycieku pamięci. Typowe przykłady to ZipInputStream lub DirectoryStream.

Jvmti agentów, w szczególności jdwp debugging agent-może również powodować nadmierne zużycie pamięci.

Ta odpowiedź opisuje jak profilować natywne alokacje pamięci za pomocą async-profiler.

Alokator zagadnienia

Proces zwykle żąda pamięci natywnej bezpośrednio z systemu operacyjnego (wywołaniem systemowym mmap) lub przy użyciu standardowego alokatora libc malloc. Z kolei malloc żąda dużych kawałków pamięci od systemu operacyjnego za pomocą mmap, a następnie zarządza tymi kawałkami według własnego algorytmu alokacji. Problem w tym, że algorytm ten może prowadzić do fragmentacji i nadmiernego wykorzystania pamięci wirtualnej.

jemalloc, alternatywny alokator, często wydaje się mądrzejszy niż zwykły libc malloc, więc przejście na jemalloc może spowodować zmniejszenie rozmiaru footprintu za darmo.

Podsumowanie

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

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 pewnych obszarów pamięci (takich jak pamięć podręczna kodu) przez flagi JVM, ale wiele innych nie jest pod kontrolą JVM.

Jednym z możliwych sposobów ustawiania limitów dokera byłoby obserwowanie rzeczywistego zużycia pamięci w "normalny" stan procesu. Istnieją narzędzia i techniki do badania problemów z zużyciem pamięci Java: Native Memory Tracking, pmap, jemalloc, async-profiler .

Update

Oto zapis mojej prezentacji ślad pamięci 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 profil natywne wycieki pamięci w aplikacji Java.

 258
Author: apangin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-09-02 10:52:12

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

Dlaczego tak jest, gdy podam -Xmx=1G mój JVM zużywa więcej pamięci niż 1gb pamięci?

Podanie -Xmx=1g nakazuje JVM przydzielić 1GB sterty. Nie jest nakazanie JVM, aby ograniczył całe zużycie pamięci do 1gb. Są tabele kart, pamięci podręczne kodów i wszelkiego rodzaju inne dane struktury. Parametr, którego używasz do określenia całkowitego zużycia pamięci, to - MaxRAM. Be pamiętaj, że z-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 tworzy ciśnienia pamięci, więc GC nie musi również zwalniać zużytej pamięci. Mam nadzieję, że XX:MaxRAM pomoże Ci zmniejszyć ślad pamięci. W końcu możesz dostosować konfigurację GC(-XX:MinHeapFreeRatio,-XX:MaxHeapFreeRatio, ...)


Istnieje wiele typów metryk pamięci. Docker wydaje się raportować rozmiar pamięci RSS, który może być inny niż "zaangażowana" pamięć zgłoszona przez jcmd (starsze wersje Dockera zgłaszają RSS+cache jako użycie pamięci). Dobra dyskusja i linki: różnica między Resident Set Size (RSS) a Java total committed memory (NMT) dla JVM działającego w kontenerze Docker

(RSS) pamięć może być zjadana także przez inne narzędzia w kontenerze-powłoce, Menedżerze procesów, ... Nie wiemy, co jeszcze działa w kontenerze i jak rozpocząć procesy w kontenerze.

 17
Author: Jan Garaj,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-12-04 14:03:53

TL;DR

Szczegółowe wykorzystanie pamięci jest zapewniane przez natywne szczegóły śledzenia pamięci (NMT) (głównie metadane kodu i garbage collector). Ponadto kompilator Java i optymalizator C1/C2 zużywają pamięć nieujętą w podsumowaniu.

Ślad pamięci można zmniejszyć za pomocą znaczników JVM (ale nie ma wpływu).

Rozmiar kontenera Dockera musi zostać przeprowadzony poprzez testowanie z oczekiwanym obciążeniem aplikacji.


Szczegóły dla każdego komponenty

Dzielona przestrzeń klas może być wyłączona wewnątrz kontenera, ponieważ klasy nie będą współdzielone przez inny proces JVM. Można użyć następującej flagi. Usunie współdzieloną przestrzeń klasową (17MB).

-Xshare:off

Serial garbage collector ma minimalną ilość pamięci kosztem dłuższego czasu pauzy podczas przetwarzania garbage collect (zobacz porównanie GC na jednym obrazku ). Można ją włączyć za pomocą następujących flaga. Może zaoszczędzić do użytej przestrzeni GC (48MB).

-XX:+UseSerialGC

Kompilator C2 Może być wyłączony z poniższą flagą, aby zmniejszyć dane profilowania używane do decydowania, czy optymalizować metodę, czy nie.

-XX:+TieredCompilation -XX:TieredStopAtLevel=1

Przestrzeń kodu jest zmniejszona o 20MB. Co więcej, pamięć poza JVM jest zmniejszona o 80MB (różnica między przestrzenią NMT A przestrzenią RSS). kompilator optymalizujący C2 potrzebuje 100MB.

Kompilatory C1 i C2 można wyłączyć za pomocą następujących flaga.

-Xint

Pamięć poza JVM jest teraz niższa niż całkowita zatwierdzona przestrzeń. Przestrzeń kodu jest zmniejszona o 43MB. Uwaga, ma to duży wpływ na wydajność aplikacji. wyłączenie kompilatora C1 i C2 zmniejsza pamięć używaną o 170 MB.

Za pomocą Graal vm compiler (wymiana C2) prowadzi do nieco mniejszej ilości pamięci. Zwiększa o 20MB przestrzeń pamięci kodu i zmniejsza o 60MB z zewnątrz JVM pamięć.

Artykuł Java Memory Management for JVM zawiera kilka istotnych informacji o różnych przestrzeniach pamięci. Oracle udostępnia pewne szczegóły w natywnej dokumentacji śledzenia pamięci . Więcej szczegółów na temat poziomu kompilacji można znaleźć w advanced compilation policy oraz w disable C2 reduce code cache size by a factor 5. Kilka szczegółów na temat dlaczego JVM raportuje bardziej zaangażowaną pamięć niż rozmiar rezydenta procesu Linuksa? gdy obie Kompilatory są wyłączone.

 9
Author: Nicolas Henneaux,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-12-04 18:21:10

Java potrzebuje dużo pamięci. Sam JVM potrzebuje dużo pamięci do uruchomienia. Sterta jest pamięcią dostępną wewnątrz maszyny wirtualnej, dostępną dla Twojej aplikacji. Ponieważ JVM to duży pakiet pełen wszystkich możliwych gadżetów, wystarczy dużo pamięci, aby się załadować.

Począwszy od Javy 9 masz coś o nazwie project Jigsaw , co może zmniejszyć pamięć używaną podczas uruchamiania aplikacji java (wraz z czasem startu). Projekt jigsaw i nowy system modułów nie były koniecznie stworzony, aby zmniejszyć niezbędną pamięć, ale jeśli jest 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łowego uzyskano 21MB CLI(z wbudowanym JRE). JRE zajmuje ponad 200mb. To powinno przekładać się na mniej przydzieloną pamięć, gdy aplikacja jest włączona(wiele nieużywanych klas JRE nie będzie już załadowany).

Oto kolejny fajny tutorial: 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 tak jest najlepiej.

 0
Author: adiian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-12-01 12:02:51

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

docker run -d --name my-container --memory 500m <iamge-name>
Nie mogę odpowiedzieć na inne pytania.
 -1
Author: v_sukt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-12-05 05:22:39