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

118

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

Nicolas Henneaux
quelle
6
Auf dem Heap werden Objekte zugewiesen. Die JVM verfügt jedoch über viele andere Speicherbereiche, einschließlich gemeinsam genutzter Bibliotheken, direkter Speicherpuffer, Thread-Stapel, GUI-Komponenten und Metaspace. Sie müssen sich ansehen, wie groß die JVM sein kann, und das Limit so hoch einstellen, dass Sie den Prozess lieber abbrechen als mehr verwenden möchten.
Peter Lawrey
2
Es sieht so aus, als würde der GC viel Speicher verbrauchen. Sie können stattdessen versuchen, den CMS-Kollektor zu verwenden. Es sieht so aus, als würden ~ 125 MB für Metaspace + Code verwendet. Ohne Verkleinerung Ihrer Codebasis ist es jedoch unwahrscheinlich, dass Sie diese verkleinern können. Der festgelegte Speicherplatz befindet sich nahe an Ihrer Grenze, daher ist es nicht verwunderlich, dass er getötet wird.
Peter Lawrey
Wo / wie stellen Sie die Konfiguration -Xms und -Xmx ein?
Mick
1
Führt das Programm viele Dateivorgänge aus (z. B. erstellt es Dateien in Gigabyte-Größe)? In diesem cgroupsFall 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. ( psdocker stats
Wohlgemerkt

Antworten:

203

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)

  1. Java Heap

    Der offensichtlichste Teil. Hier leben Java-Objekte. Der Heap nimmt bis zu -Xmxviel Speicherplatz ein.

  2. 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:MarkStackSizeMaxhä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:+UseSerialGCund -XX:+UseShenandoahGChaben den kleinsten 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 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.

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

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

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

  7. 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 im OtherAbschnitt der NMT-Ausgabe (oder Internalvor JDK 11) enthalten.

Die Menge des verwendeten direkten Speichers ist über JMX sichtbar, z. B. in JConsole oder Java Mission Control:

BufferPool MBean

Neben direkten ByteBuffern kann es auch MappedByteBuffersDateien 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>

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

Der von geladene JNI-Code System.loadLibrarykann 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 sind ZipInputStreamoder DirectoryStream.

JVMTI-Agenten, insbesondere jdwpDebugging-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 mmapSystemaufruf) oder mithilfe des mallocStandard-libc-Allokators an. Fordert wiederum mallocgroße Speicherblöcke vom Betriebssystem an mmapund 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 libc malloc, sodass ein Wechsel zu jemallockostenlos 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 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.

Apangin
quelle
1
Sind seit jdk7 keine internierten Strings im Heap? ( bugs.java.com/bugdatabase/view_bug.do?bug_id=6962931 ) - Vielleicht irre ich mich.
J-Keck
5
@ j-keck String-Objekte befinden sich im Heap, aber die Hashtabelle (die Buckets und die Einträge mit Referenzen und Hash-Codes) befindet sich im Off-Heap-Speicher. Ich habe den Satz genauer umformuliert. Vielen Dank für den Hinweis.
Apangin
Selbst wenn Sie nicht direkte ByteBuffer verwenden, weist die JVM temporäre direkte Puffer im nativen Speicher zu, ohne dass Speicherbeschränkungen festgelegt sind. Vgl. evanjones.ca/java-bytebuffer-leak.html
Cpt. Senkfuss
16

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

Warum verbraucht meine JVM mehr Speicher als 1 GB Speicher, wenn ich -Xmx = 1 g spezifiziere?

Wenn Sie -Xmx = 1g angeben, wird die JVM angewiesen, einen 1-GB-Heap zuzuweisen. Die JVM wird nicht angewiesen, die gesamte Speichernutzung auf 1 GB zu beschränken. Es gibt Kartentabellen, Code-Caches und alle möglichen anderen Off-Heap-Datenstrukturen. Der Parameter, mit dem Sie die Gesamtspeicherauslastung angeben, ist -XX: MaxRAM. Beachten Sie, dass mit -XX: MaxRam = 500m Ihr Heap ungefähr 250mb beträgt.

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.

Jan Garaj
quelle
Es ist in der Tat besser mit -XX:MaxRam. Ich denke, es wird immer noch mehr als das definierte Maximum verwendet, aber es ist besser, danke!
Nicolas Henneaux
Vielleicht brauchen Sie wirklich mehr Speicher für diese Java-Instanz. Es gibt 15267 Klassen, 56 Threads.
Jan Garaj
1
Hier finden Sie weitere Details, Java-Argumente -Xmx128m -Xms128m -Xss228k -XX:MaxRAM=256m -XX:+UseSerialGC, erzeugt Docker 428.5MiB / 600MiBund jcmd 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?
Nicolas Henneaux
1
Info / Link zum RSS-Speicher hinzugefügt.
Jan Garaj
Das bereitgestellte RSS befindet sich nur innerhalb des Containers für den Java-Prozess ps -p 71 -o pcpu,rss,size,vsize, wenn der Java-Prozess die PID 71 hat. Eigentlich hat -XX:MaxRamdas nicht geholfen, aber der von Ihnen bereitgestellte Link hilft bei der seriellen GC.
Nicolas Henneaux
8

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.

-Xshare:off

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.

-XX:+UseSerialGC

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.

-XX:+TieredCompilation -XX:TieredStopAtLevel=1

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.

-Xint

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.

Nicolas Henneaux
quelle
-1

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.

adiian
quelle
Die Verwendung jlinkist recht restriktiv, da die Anwendung modularisiert werden muss. Das automatische Modul wird nicht unterstützt, daher gibt es keinen einfachen Weg dorthin.
Nicolas Henneaux
-1

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

docker run -d --name my-container --memory 500m <iamge-name>

kann keine anderen Fragen beantworten.

v_sukt
quelle