it-swarm.com.de

Java verwendet viel mehr Speicher als Heap-Größe (oder Größe korrekt Docker-Speicherlimit)

Für meine Anwendung ist der vom Java-Prozess verwendete Speicher viel mehr als die Heap-Größe.

Das System, in dem die Container ausgeführt werden, hat ein Speicherproblem, da der Container viel mehr Speicher als die Heap-Größe beansprucht.

Die Größe des Heapspeichers ist auf 128 MB (-Xmx128m -Xms128m) festgelegt, während der Container bis zu 1 GB Speicher belegt. Unter normalen Bedingungen benötigt es 500 MB. Wenn der Docker-Container eine Grenze unterhalb (z. B. mem_limit=mem_limit=400MB) hat, wird der Prozess durch den Killer des Betriebssystems beendet.

Könnten Sie erklären, warum der Java-Prozess viel mehr Speicher als der Heapspeicher benötigt? Wie passt man die Docker-Speichergrenze richtig an? Gibt es eine Möglichkeit, den Speicherbedarf des Java-Prozesses außerhalb des Heapspeichers zu reduzieren?


Ich sammle einige Details über das Problem mit dem Befehl von Native Memory Tracking in JVM .

Vom Host-System bekomme ich den vom Container belegten Speicherplatz.

$ 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

Aus dem Container heraus bekomme ich den Speicher, der vom Prozess verwendet wird.

$ 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

Die Anwendung ist ein Webserver, der Jetty/Jersey/CDI in einem fetten Umfang von 36 MB verwendet.

Folgende Betriebssystem- und Java-Versionen werden verwendet (innerhalb des Containers). Das Docker-Image basiert auf 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

60

Virtueller Speicher, der von einem Java Prozess verwendet wird, geht weit über nur Java Heap hinaus. Sie wissen, JVM enthält viele Untersysteme: Garbage Collector, Class Loading, JIT-Compiler usw. , und all diese Subsysteme benötigen eine bestimmte Menge von RAM, um zu funktionieren.

JVM ist nicht der einzige RAM-Konsument. Native Bibliotheken (einschließlich der Standard-Klassenbibliothek Java)) können auch nativen Speicher zuweisen. Dies ist für die native Speicherüberwachung nicht sichtbar. Java) kann von der Anwendung selbst ausgeführt werden Verwenden Sie auch Off-Heap-Speicher mithilfe von direkten ByteBuffers.

Also, was braucht Speicher in einem Java Prozess?

JVM-Teile (meistens von Native Memory Tracking gezeigt)

  1. Java Heap

    Der offensichtlichste Teil. Hier leben Java Objekte. Heap belegt bis zu -Xmx Speicher.

  2. Müllsammler

    GC-Strukturen und -Algorithmen erfordern zusätzlichen Speicher für das Heap-Management. Diese Strukturen sind Mark Bitmap, Mark Stack (zum Durchlaufen von Objektgraphen), Remembered Sets (zum Aufzeichnen von Interregion-Referenzen) und andere. Einige von ihnen sind direkt einstellbar, z. -XX:MarkStackSizeMax, Andere hängen vom Heap-Layout ab, z. Je größer die G1-Regionen (-XX:G1HeapRegionSize) sind, desto kleiner sind die gespeicherten Mengen.

    Der Overhead des GC-Speichers variiert zwischen den GC-Algorithmen. -XX:+UseSerialGC Und -XX:+UseShenandoahGC Haben den geringsten Overhead. G1 oder CMS können leicht etwa 10% der gesamten Heap-Größe verwenden.

  3. Code-Cache

    Enthält dynamisch generierten Code: JIT-kompilierte Methoden, Interpreter und Laufzeitstubs. Seine Größe ist begrenzt durch -XX:ReservedCodeCacheSize (Standardmäßig 240M). Deaktivieren Sie -XX:-TieredCompilation, Um die Menge des kompilierten Codes und damit die Nutzung des Code-Cache zu reduzieren.

  4. Compiler

    Der JIT-Compiler selbst benötigt auch Speicher, um seine Arbeit zu erledigen. Dies kann durch Ausschalten von Tiered Compilation oder durch Reduzieren der Anzahl der Compiler-Threads wieder reduziert werden: -XX:CICompilerCount.

  5. Laden der Klasse

    Klassenmetadaten (Methodenbytecodes, Symbole, Konstantenpools, Anmerkungen usw.) werden im Off-Heap-Bereich namens Metaspace gespeichert. Je mehr Klassen geladen sind, desto mehr Metaspace wird verwendet. Die Gesamtnutzung kann durch -XX:MaxMetaspaceSize (Standardmäßig unbegrenzt) und -XX:CompressedClassSpaceSize (Standardmäßig 1 GB) begrenzt werden.

  6. Symboltabellen

    Zwei Haupt-Hashtabellen der JVM: Die Symbol-Tabelle enthält Namen, Signaturen, Bezeichner usw. und die String-Tabelle enthält Verweise auf internierte Strings. Wenn Native Memory Tracking eine signifikante Speichernutzung durch eine String-Tabelle anzeigt, bedeutet dies wahrscheinlich, dass die Anwendung übermäßig String.intern Aufruft.

  7. Themen

    Thread-Stapel sind auch für die Entnahme des Arbeitsspeichers verantwortlich. Die Stapelgröße wird durch -Xss Gesteuert. Der Standard ist 1M pro Thread, aber zum Glück sind die Dinge nicht so schlimm. Das Betriebssystem weist Speicherseiten träge zu, d. H. Bei der ersten Verwendung, sodass die tatsächliche Speichernutzung viel geringer ist (normalerweise 80 bis 200 KB pro Threadstapel). Ich habe ein Skript geschrieben, um zu schätzen, wie viel RSS zu Java Thread-Stacks) gehört.

    Es gibt andere JVM-Teile, die nativen Speicher zuordnen, die jedoch normalerweise keine große Rolle für den Gesamtspeicherbedarf spielen.

Direkte Puffer

Eine Anwendung kann explizit Off-Heap-Speicher anfordern, indem sie ByteBuffer.allocateDirect Aufruft. Das Standard-Off-Heap-Limit ist gleich -Xmx, Kann aber mit -XX:MaxDirectMemorySize Überschrieben werden. Direkte ByteBuffer sind im Abschnitt Other der NMT-Ausgabe (oder Internal vor JDK 11) enthalten.

Die Menge des verwendeten Direktspeichers ist durch JMX sichtbar, z. in JConsole oder Java Mission Control:

BufferPool MBean

Neben direkten Byte-Puffern kann es MappedByteBuffers geben - die Dateien, die dem virtuellen Speicher eines Prozesses zugeordnet sind. NMT verfolgt sie nicht, MappedByteBuffers können jedoch auch physischen Speicher belegen. Und es gibt keine einfache Möglichkeit, die Einnahme einzuschränken. Sie können die tatsächliche Verwendung einfach anhand der Prozessspeicherzuordnung sehen: 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
                           ^^^^^               ^^^^^^^^^^^^^^^^^^^^^^^^

Native Bibliotheken

JNI-Code, der mit System.loadLibrary Geladen wurde, kann so viel Speicher außerhalb des Heapspeichers reservieren, wie er möchte, ohne dass die JVM-Seite dies steuert. Dies betrifft auch die Standardklassenbibliothek Java). Insbesondere nicht geschlossene Ressourcen Java) können zu einem systemeigenen Speicherverlust führen. Typische Beispiele sind ZipInputStream oder DirectoryStream.

JVMTI-Agenten, insbesondere der Debugging-Agent jdwp, können ebenfalls einen übermäßigen Speicherverbrauch verursachen.

Diese Antwort beschreibt, wie native Speicherzuordnungen mit async-profiler profiliert werden.

Allocator-Probleme

Ein Prozess fordert normalerweise systemeigenen Speicher entweder direkt vom Betriebssystem an (durch mmap Systemaufruf) oder mithilfe von malloc - Standard-libc-Zuweisung. Im Gegenzug fordert malloc mit mmap große Speicherblöcke vom Betriebssystem an und verwaltet diese Blöcke dann gemäß seinem eigenen Zuordnungsalgorithmus. Das Problem ist - dieser Algorithmus kann zu Fragmentierung führen und übermäßige Nutzung des virtuellen Speichers .

jemalloc , ein alternativer Allokator, erscheint häufig schlauer als die reguläre libc malloc, sodass ein Wechsel zu jemalloc zu einem geringeren Platzbedarf führen kann.

Fazit

Es gibt keine garantierte Möglichkeit, die vollständige Speichernutzung eines Java) - Prozesses abzuschätzen, da zu viele Faktoren zu berücksichtigen sind.

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

Es ist möglich, bestimmte Speicherbereiche (wie den Code-Cache) durch JVM-Flags zu verkleinern oder einzuschränken, aber viele andere befinden sich überhaupt außerhalb der JVM-Kontrolle.

Ein möglicher Ansatz zum Festlegen von Docker-Grenzwerten wäre, die tatsächliche Speichernutzung in einem "normalen" Zustand des Prozesses zu beobachten. Es gibt Tools und Techniken zur Untersuchung von Problemen mit Java Speicherverbrauch: Native Memory Tracking , pmap , jemalloc , Async-Profiler .

152
apangin

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

Warum ist es, wenn ich -Xmx = 1g angebe, verbraucht meine JVM mehr Speicher als 1 GB der Erinnerung?

Durch die Angabe von -Xmx = 1g wird die JVM angewiesen, einen 1-GB-Heapspeicher zuzuweisen. Es ist nicht die JVM anweisen, die gesamte Speicherbelegung auf 1 GB zu begrenzen. Es gibt Kartentabellen, Code-Caches und alle Arten anderer off-heap-Daten Strukturen. Der Parameter, den Sie zur Angabe der Gesamtspeicherbelegung verwenden, ist -XX: MaxRAM. Beachten Sie, dass mit -XX: MaxRam = 500m Ihr Heap ungefähr 250 MB beträgt.

Java erkennt die Größe des Hostspeichers und kennt keine Einschränkungen des Containerspeichers. Dadurch wird kein Speicherdruck erzeugt, sodass der verwendete Speicher nicht freigegeben werden muss. Ich hoffe, XX:MaxRAM wird Ihnen helfen, den Speicherbedarf zu verringern. Eventuell können Sie die GC-Konfiguration anpassen (-XX:MinHeapFreeRatio, -XX:MaxHeapFreeRatio, ...) 


Es gibt viele Arten von Speichermetriken. Offenbar meldet Docker die Größe des RSS-Speichers, die sich von dem von jcmd gemeldeten "festgeschriebenen" Speicher unterscheiden kann (ältere Versionen von Docker melden RSS + -Cache als Speichernutzung). Gute Diskussion und Links: Unterschied zwischen Resident Set Size (RSS) und insgesamt festgeschriebenem Java-Speicher (NMT) für eine JVM, die in Docker-Container ausgeführt wird

(RSS) -Speicher kann auch von anderen Dienstprogrammen im Container belegt werden - Shell, Prozessmanager, ... Wir wissen nicht, was im Container sonst noch läuft und wie Sie Prozesse im Container starten.

10
Jan Garaj

TL; DR

Die detaillierte Verwendung des Speichers wird durch Native Memory Tracking (NMT) -Details (hauptsächlich Code-Metadaten und Garbage-Collector) bereitgestellt. Darüber hinaus verbrauchen der Java-Compiler und -Optimierer C1/C2 den nicht in der Zusammenfassung angegebenen Speicher.

Der Speicherbedarf kann mithilfe von JVM-Flags reduziert werden (es gibt jedoch Auswirkungen).

Die Docker-Containergröße muss durch Testen mit der erwarteten Auslastung der Anwendung erfolgen.


Detail für jede Komponente

Der shared class space kann in einem Container deaktiviert werden, da die Klassen nicht von einem anderen JVM-Prozess gemeinsam genutzt werden. Das folgende Flag kann verwendet werden. Der gemeinsam genutzte Klassenraum (17 MB) wird entfernt.

-Xshare:off

Der Garbage Collector serial hat einen minimalen Speicherbedarf auf Kosten einer längeren Pausenzeit während der Garbage Collect-Verarbeitung (siehe Aleksey Shipilëv Vergleich zwischen GC in einem Bild ). Es kann mit dem folgenden Flag aktiviert werden. Es kann bis zu dem verwendeten GC-Speicherplatz (48 MB) eingespart werden.

-XX:+UseSerialGC

Der C2-Compiler kann mit dem folgenden Flag deaktiviert werden, um die Profilierungsdaten für die Entscheidung, ob eine Methode optimiert werden soll, zu reduzieren.

-XX:+TieredCompilation -XX:TieredStopAtLevel=1

Der Code-Speicherplatz wird um 20 MB reduziert. Darüber hinaus reduziert sich der Speicher außerhalb der JVM um 80 MB (Differenz zwischen NMT-Speicherplatz und RSS-Speicherplatz). Der Optimierungscompiler C2 benötigt 100 MB.

Die C1- und C2-Compiler können mit dem folgenden Flag deaktiviert werden.

-Xint

Der Speicher außerhalb der JVM ist jetzt niedriger als der insgesamt zugesagte Speicherplatz. Der Code-Speicherplatz wird um 43 MB reduziert. Achtung, dies hat einen großen Einfluss auf die Leistung der Anwendung. Durch Deaktivieren des C1- und C2-Compilers wird der Speicher um 170 MB reduziert.

Die Verwendung von Graal VM - Compiler (Ersetzen von C2) führt zu etwas geringerem Speicherbedarf. Es erhöht den Codespeicherplatz um 20 MB und verringert sich um 60 MB außerhalb des JVM-Speichers.

Der Artikel Java Memory Management für JVM enthält einige relevante Informationen zu den verschiedenen Speicherbereichen Oracle enthält einige Details in Native Memory Tracking-Dokumentation . Weitere Informationen zur Kompilierstufe in erweiterte Kompilierungsrichtlinie und in deaktivieren C2 die Code-Cache-Größe um einen Faktor 5 reduzieren. Einige Details zu Warum meldet eine JVM mehr Arbeitsspeicher als die festgelegte Linux-Prozessgröße? wenn beide Compiler deaktiviert sind.

4

In den obigen Antworten erfahren Sie, warum die JVM so viel Speicher benötigt, aber vielleicht benötigen Sie eine Lösung. Diese Artikel helfen dabei:
- https://blogs.Oracle.com/Java-platform-group/Java-se-support-for-docker-cpu-und-speicherklimits
- https://royvanrijn.com/blog/2018/05/Java-and-docker-memory-limits/

0
Jason Wong