Der von einem Java-Prozess verwendete virtuelle Speicher geht weit über den bloßen Java-Heap hinaus. Sie wissen, dass JVM viele Subsysteme enthält:Garbage Collector, Class Loading, JIT-Compiler usw., und alle diese Subsysteme benötigen eine bestimmte Menge an RAM, um zu funktionieren.
JVM ist nicht der einzige Verbraucher von RAM. Native Bibliotheken (einschließlich der Standard-Java-Klassenbibliothek) können auch nativen Speicher zuweisen. Und dies wird nicht einmal für Native Memory Tracking sichtbar sein. Die Java-Anwendung selbst kann über direkte ByteBuffers auch Off-Heap-Speicher verwenden.
Was braucht also Speicher in einem Java-Prozess?
JVM-Teile (hauptsächlich durch Native Memory Tracking angezeigt)
- Java-Heap
Der offensichtlichste Teil. Hier leben Java-Objekte. Heap nimmt bis zu -Xmx
auf Speicherplatz.
- Garbage Collector
GC-Strukturen und -Algorithmen erfordern zusätzlichen Speicher für die Heap-Verwaltung. Diese Strukturen sind Mark Bitmap, Mark Stack (zum Durchlaufen von Objektgraphen), Remembered Sets (zum Aufzeichnen von Referenzen zwischen Regionen) und andere. Einige von ihnen sind direkt abstimmbar, z. -XX:MarkStackSizeMax
, andere hängen vom Heap-Layout ab, z. die größeren sind G1-Regionen (-XX:G1HeapRegionSize
), desto kleiner sind die erinnerten Mengen.
Der GC-Speicher-Overhead variiert zwischen den GC-Algorithmen. -XX:+UseSerialGC
und -XX:+UseShenandoahGC
den geringsten Overhead haben. 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. Seine Größe ist durch -XX:ReservedCodeCacheSize
begrenzt (standardmäßig 240 MB). Deaktivieren Sie -XX:-TieredCompilation
um die Menge an kompiliertem Code und damit die Code-Cache-Nutzung zu reduzieren.
- Compiler
Der JIT-Compiler selbst benötigt auch Speicher, um seine Arbeit zu erledigen. Dies kann wieder reduziert werden, indem man Tiered Compilation ausschaltet oder die Anzahl der Compiler-Threads reduziert:-XX:CICompilerCount
.
- Klasse wird geladen
Klassen-Metadaten (Methoden-Bytecodes, Symbole, Konstantenpools, Anmerkungen usw.) werden in einem Off-Heap-Bereich namens Metaspace gespeichert. Je mehr Klassen geladen werden, desto mehr Metaspace wird verwendet. Die Gesamtnutzung kann um -XX:MaxMetaspaceSize
begrenzt werden (standardmäßig unbegrenzt) und -XX:CompressedClassSpaceSize
(1G standardmäßig).
- Symboltabellen
Zwei Haupt-Hashtables der JVM:Die Symbol-Tabelle enthält Namen, Signaturen, Identifikatoren usw. und die String-Tabelle enthält Verweise auf interne Strings. Wenn die Überwachung des nativen Speichers eine erhebliche Speichernutzung durch eine Zeichenfolgentabelle anzeigt, bedeutet dies wahrscheinlich, dass die Anwendung zu häufig String.intern
aufruft .
- Fäden
Thread-Stacks sind auch dafür verantwortlich, RAM zu belegen. Die Stapelgröße wird durch -Xss
gesteuert . Der Standardwert ist 1M pro Thread, aber zum Glück sind die Dinger 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–200 KB pro Thread-Stack). 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 sie spielen normalerweise keine große Rolle beim Gesamtspeicherverbrauch.
Direkte Puffer
Eine Anwendung kann explizit Off-Heap-Speicher anfordern, indem sie ByteBuffer.allocateDirect
aufruft . Das standardmäßige Off-Heap-Limit ist gleich -Xmx
, kann aber mit -XX:MaxDirectMemorySize
überschrieben werden . Direkte ByteBuffer sind in Other
enthalten Abschnitt der NMT-Ausgabe (oder Internal
vor JDK 11).
Die Menge des verwendeten Direktspeichers ist über JMX sichtbar, z. in JConsole oder Java Mission Control:
Neben direkten ByteBuffern 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 beanspruchen. Und es gibt keinen einfachen Weg, um zu begrenzen, wie viel sie nehmen können. Sie können die tatsächliche Nutzung einfach sehen, indem Sie sich die Prozessspeicherkarte ansehen: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 geladen von System.loadLibrary
kann so viel Off-Heap-Speicher zuweisen, wie es möchte, ohne Kontrolle von der JVM-Seite. Dies betrifft auch die Standard-Java-Klassenbibliothek. Insbesondere nicht geschlossene Java-Ressourcen können zu einer Quelle für native Speicherlecks werden. Typische Beispiele sind ZipInputStream
oder DirectoryStream
.
JVMTI-Agenten, insbesondere jdwp
Debugging-Agent - kann auch zu übermäßigem Speicherverbrauch führen.
Diese Antwort beschreibt, wie native Speicherzuweisungen mit async-profiler profiliert werden.
Allocator-Probleme
Ein Prozess fordert normalerweise nativen Speicher entweder direkt vom Betriebssystem (durch mmap
Systemaufruf) oder mit malloc
- Standard-Libc-Zuweisung. Im Gegenzug malloc
fordert große Speicherblöcke vom Betriebssystem mit mmap
an , und verwaltet diese Chunks dann gemäß einem eigenen Zuordnungsalgorithmus. Das Problem ist - dieser Algorithmus kann zu Fragmentierung und übermäßiger Nutzung des virtuellen Speichers führen.
jemalloc
, ein alternativer Zuordner, erscheint oft intelligenter als die normale libc malloc
, wechsle also zu jemalloc
kann kostenlos zu einem geringeren Platzbedarf führen.
Schlussfolgerung
Es gibt keine garantierte Möglichkeit, die vollständige Speichernutzung eines Java-Prozesses abzuschätzen, da zu viele Faktoren berücksichtigt werden müssen.
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 Code-Cache) durch JVM-Flags zu verkleinern oder zu begrenzen, aber viele andere sind überhaupt außerhalb der JVM-Kontrolle.
Ein möglicher Ansatz zum Festlegen von Docker-Limits wäre, die tatsächliche Speichernutzung in einem "normalen" Zustand des Prozesses zu beobachten. 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 of a Java Process.
In diesem Video bespreche ich, was Speicher in einem Java-Prozess verbrauchen kann, wie die Größe bestimmter Speicherbereiche überwacht und eingeschränkt wird und wie native Speicherlecks in einer Java-Anwendung profiliert werden.
https://developers.redhat.com/blog/2017/04/04/openjdk-and-containers/:
Warum verbraucht meine JVM mehr Speicher als 1 GB Speicher, wenn ich -Xmx=1g angebe?
Die Angabe von -Xmx=1g weist die JVM an, einen 1-GB-Heap zuzuweisen. Es weist die JVM nicht an, die gesamte Speichernutzung auf 1 GB zu begrenzen. Es gibt Kartentabellen, Code-Caches und alle möglichen anderen Off-Heap-Datenstrukturen. Der Parameter, den Sie verwenden, um die Gesamtspeicherauslastung anzugeben, ist-XX:MaxRAM. Beachten Sie, dass Ihr Heap mit -XX:MaxRam=500m ungefähr 250 MB groß sein wird.
Java sieht die Größe des Hostspeichers und kennt keine Beschränkungen des Containerspeichers. Es erzeugt keinen Speicherdruck, sodass GC auch keinen verwendeten Speicher freigeben muss. Ich hoffe XX:MaxRAM
wird Ihnen helfen, den Speicherbedarf zu reduzieren. Schließlich können Sie die GC-Konfiguration anpassen (-XX:MinHeapFreeRatio
,-XX:MaxHeapFreeRatio
, ...)
Es gibt viele Arten von Speichermetriken. Docker scheint die Größe des RSS-Speichers zu melden, 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 Java Total Committed Memory (NMT) für eine JVM, die im Docker-Container läuft
(RSS)-Speicher kann auch von einigen anderen Dienstprogrammen im Container gefressen werden - Shell, Prozessmanager, ... Wir wissen nicht, was sonst noch im Container läuft und wie Sie Prozesse im Container starten.
TL;DR
Die genaue Nutzung des Arbeitsspeichers 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 Speicher, der nicht in der Zusammenfassung angegeben ist.
Der Speicherbedarf kann mithilfe von JVM-Flags reduziert werden (aber es gibt Auswirkungen).
Die Dimensionierung des Docker-Containers muss durch Tests mit der erwarteten Last der Anwendung erfolgen.
Detail für jede Komponente
Der gemeinsame Klassenbereich kann innerhalb eines Containers deaktiviert werden, da die Klassen nicht von einem anderen JVM-Prozess gemeinsam genutzt werden. Das folgende Flag kann verwendet werden. Dadurch wird der gemeinsam genutzte Klassenbereich (17 MB) entfernt.
-Xshare:off
Der Müllsammler serial hat einen minimalen Speicherbedarf auf Kosten einer längeren Pausenzeit während der Garbage-Collect-Verarbeitung (siehe Aleksey Shipilëvs Vergleich zwischen GC in einem Bild). Es kann mit dem folgenden Flag aktiviert werden. Es kann bis zum verwendeten GC-Speicherplatz (48 MB) eingespart werden.
-XX:+UseSerialGC
Der C2-Compiler kann mit dem folgenden Flag deaktiviert werden, um die Profildaten zu reduzieren, die verwendet werden, um zu entscheiden, ob eine Methode optimiert werden soll oder nicht.
-XX:+TieredCompilation -XX:TieredStopAtLevel=1
Der Codespace wird um 20 MB reduziert. Außerdem wird der Speicher außerhalb der JVM um 80 MB reduziert (Differenz zwischen NMT-Speicherplatz und RSS-Speicherplatz). Der optimierende Compiler C2 benötigt 100 MB.
Die C1- und C2-Compiler kann mit dem folgenden Flag deaktiviert werden.
-Xint
Der Speicher außerhalb der JVM ist jetzt kleiner als der gesamte zugesagte Speicherplatz. Der Codespace wird um 43 MB reduziert. Beachten Sie, dass dies einen großen Einfluss auf die Leistung der Anwendung hat. Das Deaktivieren des C1- und C2-Compilers reduziert den verwendeten Speicher um 170 MB.
Verwendung des Graal VM-Compilers (Ersatz von C2) führt zu einem etwas kleineren Speicherbedarf. Es erhöht den Code-Speicherplatz um 20 MB und verringert den externen JVM-Speicher um 60 MB.
Der Artikel Java Memory Management for JVM enthält einige relevante Informationen zu den verschiedenen Speicherbereichen. Oracle bietet einige Details in der Dokumentation zum Native Memory Tracking. Weitere Details zur Kompilierungsebene in der erweiterten Kompilierungsrichtlinie und in der Deaktivierung von C2 reduzieren die Code-Cache-Größe um den Faktor 5. Einige Details zu Warum meldet eine JVM mehr zugesicherten Speicher als die Größe des residenten Linux-Prozesses? wenn beide Compiler deaktiviert sind.