GNU/Linux >> LINUX-Kenntnisse >  >> Linux

Java verwendet viel mehr Speicher als die Heap-Größe (oder die richtige Größe des Docker-Speicherlimits)

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)

  1. Java-Heap

Der offensichtlichste Teil. Hier leben Java-Objekte. Heap nimmt bis zu -Xmx auf Speicherplatz.

  1. 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.

  1. 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.

  1. 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 .

  1. 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).

  1. 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 .

  1. 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.


Linux
  1. Linux – Warum zeigt Linux sowohl mehr als auch weniger Speicher an, als ich physisch installiert habe?

  2. Ersetzen Sie ein Wort oder eine Zahl in einem Satz, der kleiner als 8 ist, mit Awk oder Sed?

  3. Warum verwenden alle Docker?

  4. PHP-Speicherlimit

  5. CentOS / RHEL 6 :So begrenzen Sie Speicherressourcen für einen bestimmten Benutzer mithilfe von cgroups

So legen Sie das Docker-Speicher- und CPU-Nutzungslimit fest

Verwenden des kostenlosen Linux-Befehls

Speicherlimit und CPU-Limit im Docker-Container

Wie kann die Speichernutzung der Anwendung begrenzt werden?

Wie kann man mehr Platz zuweisen, um zu tauschen und seine Größe größer als Ram zu erhöhen?

Überprüfen Sie, ob die Dateigröße größer als 1 MB ist, indem Sie die IF-Bedingung verwenden