Für meine Anwendung ist der vom Java-Prozess verwendete Speicher viel größer als die Heap-Größe.
Das System, auf dem die Container ausgeführt werden, weist Speicherprobleme auf, da der Container viel mehr Speicher als die Heap-Größe benötigt.
Die Heap-Größe ist auf 128 MB ( -Xmx128m -Xms128m
) festgelegt, während der Container bis zu 1 GB Speicher benötigt. Unter normalen Bedingungen benötigt es 500 MB. Wenn der Docker-Container ein Limit unter (z. B. mem_limit=mem_limit=400MB
) hat, wird der Prozess vom Killer für nicht genügend Speicher des Betriebssystems beendet.
Können Sie erklären, warum der Java-Prozess viel mehr Speicher als der Heap verwendet? Wie kann ich das Docker-Speicherlimit richtig dimensionieren? Gibt es eine Möglichkeit, den Speicherbedarf außerhalb des Heapspeichers des Java-Prozesses zu verringern?
Ich sammle einige Details zu dem Problem mit dem Befehl von Native Memory Tracking in JVM .
Vom Host-System erhalte ich den vom Container verwendeten Speicher.
$ 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 Inneren des Containers erhalte ich den vom Prozess verwendeten Speicher.
$ 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 mit Jetty / Jersey / CDI, der in einem fetten Format von 36 MB gebündelt ist.
Die folgende Version von OS und Java wird 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
cgroups
Fall sollten Sie wissen, dass dem verwendeten Speicher ein Festplatten-Cache hinzugefügt wird - auch wenn dieser vom Kernel verarbeitet wird und für das Benutzerprogramm nicht sichtbar ist. (ps
docker stats
Antworten:
Der von einem Java-Prozess verwendete virtuelle Speicher geht weit über Java Heap hinaus. Sie wissen, JVM enthält viele Untersysteme: Garbage Collector, Class Loading, JIT-Compiler usw. Alle diese Subsysteme benötigen eine bestimmte Menge an RAM, um zu funktionieren.
JVM ist nicht der einzige RAM-Konsument. Native Bibliotheken (einschließlich der Standard-Java-Klassenbibliothek) können auch nativen Speicher zuweisen. Und dies ist für Native Memory Tracking nicht einmal sichtbar. Die Java-Anwendung selbst kann auch Off-Heap-Speicher mithilfe von direkten ByteBuffern verwenden.
Was braucht Speicher in einem Java-Prozess?
JVM-Teile (meistens durch Native Memory Tracking angezeigt)
Java Heap
Der offensichtlichste Teil. Hier leben Java-Objekte. Der Heap nimmt bis zu
-Xmx
viel Speicherplatz ein.Müllsammler
GC-Strukturen und -Algorithmen benötigen zusätzlichen Speicher für die Heap-Verwaltung. Diese Strukturen sind Mark Bitmap, Mark Stack (zum Durchlaufen des Objektdiagramms), Remembered Sets (zum Aufzeichnen von Referenzen zwischen Regionen) und andere. Einige von ihnen sind direkt einstellbar, z. B.
-XX:MarkStackSizeMax
hängen andere vom Heap-Layout ab. Je größer die G1-Regionen (-XX:G1HeapRegionSize
) sind, desto kleiner sind die gespeicherten Mengen.Der GC-Speicheraufwand variiert zwischen den GC-Algorithmen.
-XX:+UseSerialGC
und-XX:+UseShenandoahGC
haben den kleinsten 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 Laufzeit-Stubs. Die Größe ist begrenzt auf
-XX:ReservedCodeCacheSize
(standardmäßig 240 MB). Deaktivieren Sie diese Option-XX:-TieredCompilation
, um die Menge des kompilierten Codes und damit die Verwendung des Code-Cache zu verringern.Compiler
Der JIT-Compiler selbst benötigt für seine Arbeit auch Speicher. Dies kann wieder reduziert werden, indem Tiered Compilation deaktiviert oder die Anzahl der Compiler-Threads reduziert wird :
-XX:CICompilerCount
.Klassenladen
Klassenmetadaten (Methodenbytecodes, Symbole, konstante Pools, Anmerkungen usw.) werden im Off-Heap-Bereich namens Metaspace gespeichert. Je mehr Klassen geladen werden, desto mehr Metaspace wird verwendet. Die Gesamtnutzung kann durch
-XX:MaxMetaspaceSize
(standardmäßig unbegrenzt) und-XX:CompressedClassSpaceSize
(standardmäßig 1G) begrenzt werden.Symboltabellen
Zwei Haupt-Hashtabellen der JVM: Die Symboltabelle 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 aufruft
String.intern
.Themen
Thread-Stacks sind auch für die RAM-Speicherung verantwortlich. Die Stapelgröße wird von gesteuert
-Xss
. Der Standardwert ist 1 Million pro Thread, aber zum Glück sind die Dinge nicht so schlecht. Das Betriebssystem weist Speicherseiten träge zu, dh bei der ersten Verwendung, sodass die tatsächliche Speichernutzung viel geringer ist (normalerweise 80-200 KB pro Thread-Stapel). Ich habe ein Skript geschrieben, um abzuschätzen, wie viel RSS zu Java-Thread-Stacks gehört.Es gibt andere JVM-Teile, die nativen Speicher zuweisen, aber normalerweise spielen sie keine große Rolle für den gesamten Speicherverbrauch.
Direkte Puffer
Eine Anwendung kann explizit Off-Heap-Speicher durch Aufrufen anfordern
ByteBuffer.allocateDirect
. Das Standard-Off-Heap-Limit ist gleich-Xmx
, kann jedoch mit überschrieben werden-XX:MaxDirectMemorySize
. Direkte ByteBuffer sind imOther
Abschnitt der NMT-Ausgabe (oderInternal
vor JDK 11) enthalten.Die Menge des verwendeten direkten Speichers ist über JMX sichtbar, z. B. in JConsole oder Java Mission Control:
Neben direkten ByteBuffern kann es auch
MappedByteBuffers
Dateien geben, die dem virtuellen Speicher eines Prozesses zugeordnet sind. NMT verfolgt sie nicht, MappedByteBuffers können jedoch auch physischen Speicher belegen. Und es gibt keinen einfachen Weg, um zu begrenzen, wie viel sie aufnehmen können. Sie können die tatsächliche Nutzung nur anhand der Prozessspeicherzuordnung anzeigen:pmap -x <pid>
Native Bibliotheken
Der von geladene JNI-Code
System.loadLibrary
kann ohne Kontrolle von der JVM-Seite so viel Off-Heap-Speicher zuweisen, wie er möchte. Dies betrifft auch die Standard-Java-Klassenbibliothek. Insbesondere können nicht geschlossene Java-Ressourcen zu einer Quelle für nativen Speicherverlust werden. Typische Beispiele sindZipInputStream
oderDirectoryStream
.JVMTI-Agenten, insbesondere
jdwp
Debugging-Agenten, können ebenfalls einen übermäßigen Speicherverbrauch verursachen.Diese Antwort beschreibt, wie native Speicherzuordnungen mit dem Async-Profiler profiliert werden .
Allokatorprobleme
Ein Prozess fordert normalerweise nativen Speicher entweder direkt vom Betriebssystem (per
mmap
Systemaufruf) oder mithilfe desmalloc
Standard-libc-Allokators an. Fordert wiederummalloc
große Speicherblöcke vom Betriebssystem anmmap
und verwaltet diese Blöcke dann gemäß seinem eigenen Zuordnungsalgorithmus. Das Problem ist - dieser Algorithmus kann zu Fragmentierung und übermäßiger Nutzung des virtuellen Speichers führen .jemalloc
, ein alternativer Allokator, erscheint häufig intelligenter als die normale libcmalloc
, sodass ein Wechsel zujemalloc
kostenlos 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.
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 besteht darin, die tatsächliche Speichernutzung in einem "normalen" Zustand des Prozesses zu überwachen. Es gibt Tools und Techniken zur Untersuchung von Problemen mit dem Java-Speicherverbrauch: Native Memory Tracking , pmap , jemalloc , async- profiler .
Aktualisieren
Hier ist eine Aufzeichnung meiner Präsentation Memory Footprint eines Java-Prozesses .
In diesem Video werde ich erläutern, was in einem Java-Prozess möglicherweise Speicher verbraucht, wie die Größe bestimmter Speicherbereiche überwacht und eingeschränkt wird und wie native Speicherlecks in einer Java-Anwendung profiliert werden.
quelle
https://developers.redhat.com/blog/2017/04/04/openjdk-and-containers/ :
Java erkennt die Größe des Hostspeichers und kennt keine Einschränkungen des Containerspeichers. Es erzeugt keinen Speicherdruck, so dass GC auch keinen verwendeten Speicher freigeben muss. Ich hoffe
XX:MaxRAM
, Sie können den Speicherbedarf reduzieren. Schließlich können Sie GC - Konfiguration optimieren (-XX:MinHeapFreeRatio
,-XX:MaxHeapFreeRatio
, ...)Es gibt viele Arten von Speichermetriken. Docker scheint eine RSS-Speichergröße zu melden, die sich von der von "Commited" gemeldeten Speichergröße unterscheiden kann
jcmd
(ältere Versionen von Docker melden RSS + -Cache als Speichernutzung). Gute Diskussion und Links: Unterschied zwischen Resident Set Size (RSS) und Java Total Commited Memory (NMT) für eine JVM, die im Docker-Container ausgeführt wird(RSS) Speicher kann auch von einigen anderen Dienstprogrammen im Container verwendet werden - Shell, Prozessmanager, ... Wir wissen nicht, was sonst noch im Container ausgeführt wird und wie Sie Prozesse im Container starten.
quelle
-XX:MaxRam
. Ich denke, es wird immer noch mehr als das definierte Maximum verwendet, aber es ist besser, danke!-Xmx128m -Xms128m -Xss228k -XX:MaxRAM=256m -XX:+UseSerialGC
, erzeugtDocker 428.5MiB / 600MiB
undjcmd 58 VM.native_memory -> Native Memory Tracking: Total: reserved=1571296KB, committed=314316KB
. JVM benötigt ungefähr 300 MB, während der Container 430 MB benötigt. Wo liegen die 130 MB zwischen der JVM-Berichterstellung und der Betriebssystemberichterstattung?ps -p 71 -o pcpu,rss,size,vsize
, wenn der Java-Prozess die PID 71 hat. Eigentlich hat-XX:MaxRam
das nicht geholfen, aber der von Ihnen bereitgestellte Link hilft bei der seriellen GC.TL; DR
Die Detailverwendung des Speichers wird durch NMT-Details (Native Memory Tracking) bereitgestellt (hauptsächlich Code-Metadaten und Garbage Collector). Darüber hinaus verbrauchen der Java-Compiler und Optimierer C1 / C2 den in der Zusammenfassung nicht angegebenen Speicher.
Der Speicherbedarf kann mithilfe von JVM-Flags reduziert werden (es gibt jedoch Auswirkungen).
Die Dimensionierung des Docker-Containers muss durch Testen der Anwendung mit der erwarteten Last erfolgen.
Detail für jede Komponente
Der gemeinsam genutzte Klassenbereich 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.
Die Garbage Collector- Serie hat einen minimalen Speicherbedarf auf Kosten einer längeren Pausenzeit während der Garbage Collector- 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.
Der C2-Compiler kann mit dem folgenden Flag deaktiviert werden, um die Profildaten zu reduzieren, mit denen entschieden wird, ob eine Methode optimiert werden soll oder nicht.
Der Code-Speicherplatz wird um 20 MB reduziert. Darüber hinaus wird der Speicher außerhalb von JVM um 80 MB reduziert (Unterschied zwischen NMT-Speicherplatz und RSS-Speicherplatz). Der optimierende Compiler C2 benötigt 100MB.
Die C1- und C2-Compiler können mit dem folgenden Flag deaktiviert werden.
Der Speicher außerhalb der JVM ist jetzt niedriger als der gesamte festgeschriebene Speicherplatz. Der Code-Speicherplatz wird um 43 MB reduziert. Beachten Sie, dass dies einen großen Einfluss auf die Leistung der Anwendung hat. Durch Deaktivieren des C1- und C2-Compilers wird der Speicherbedarf um 170 MB reduziert.
Die Verwendung des Graal VM-Compilers (Ersatz von C2) führt zu einem etwas geringeren Speicherbedarf. Der Codespeicherplatz wird um 20 MB vergrößert und von außerhalb des JVM-Speichers um 60 MB verringert.
Der Artikel Java Memory Management für JVM enthält einige relevante Informationen zu den verschiedenen Speicherbereichen. Oracle bietet einige Details in der Native Memory Tracking-Dokumentation . Weitere Details zur Kompilierungsstufe in der erweiterten Kompilierungsrichtlinie und in Deaktivieren von C2 reduzieren die Code-Cache-Größe um den Faktor 5 . Einige Details zu Warum meldet eine JVM mehr festgeschriebenen Speicher als die festgelegte Größe des Linux-Prozesses? wenn beide Compiler deaktiviert sind.
quelle
Java braucht viel Speicher. JVM selbst benötigt viel Speicher, um ausgeführt zu werden. Der Heap ist der Speicher, der in der virtuellen Maschine verfügbar ist und für Ihre Anwendung verfügbar ist. Da JVM ein großes Paket mit allen möglichen Extras ist, wird nur viel Speicher benötigt, um geladen zu werden.
Ab Java 9 gibt es ein Projekt namens Jigsaw , das möglicherweise den beim Starten einer Java-App verwendeten Speicher (zusammen mit der Startzeit) reduziert. Projektpuzzle und ein neues Modulsystem wurden nicht unbedingt erstellt, um den erforderlichen Speicher zu reduzieren. Wenn es jedoch wichtig ist, können Sie es versuchen.
Sie können sich dieses Beispiel ansehen: https://steveperkins.com/using-java-9-modularization-to-ship-zero-dependency-native-apps/ . Durch die Verwendung des Modulsystems wurde eine CLI-Anwendung von 21 MB (mit eingebetteter JRE) erzielt. JRE benötigt mehr als 200 MB. Dies sollte zu weniger zugewiesenem Speicher führen, wenn die Anwendung aktiv ist (viele nicht verwendete JRE-Klassen werden nicht mehr geladen).
Hier ist ein weiteres nettes Tutorial: https://www.baeldung.com/project-jigsaw-java-modularity
Wenn Sie keine Zeit damit verbringen möchten, können Sie einfach mehr Speicher zuweisen. Manchmal ist es das Beste.
quelle
jlink
ist recht restriktiv, da die Anwendung modularisiert werden muss. Das automatische Modul wird nicht unterstützt, daher gibt es keinen einfachen Weg dorthin.Wie kann ich das Docker-Speicherlimit richtig dimensionieren? Überprüfen Sie die Anwendung, indem Sie sie einige Zeit überwachen. Um den Speicher des Containers einzuschränken, verwenden Sie die Option -m, --memory bytes für den Docker-Befehl run - oder etwas Äquivalentes, wenn Sie ihn anderweitig ausführen
kann keine anderen Fragen beantworten.
quelle