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
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?
Java Heap
Der offensichtlichste Teil. Hier leben Java Objekte. Heap belegt bis zu -Xmx
Speicher.
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.
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.
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
.
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.
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.
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.
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:
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
^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
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.
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.
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 .
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.
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.
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.
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/