301 Stimmen

Virtueller Speicherverbrauch von Java unter Linux, zu viel Speicher verwendet

Ich habe ein Problem mit einer Java-Anwendung, die unter Linux läuft.

Wenn ich die Anwendung mit der standardmäßigen maximalen Heap-Größe (64 MB) starte, sehe ich anhand der Tops-Anwendung, dass der Anwendung 240 MB virtueller Speicher zugewiesen sind. Dies führt zu einigen Problemen mit anderer Software auf dem Computer, die relativ ressourcenbeschränkt ist.

Der reservierte virtuelle Speicher wird sowieso nicht verwendet, soweit ich das verstehe, denn sobald wir das Heap-Limit erreichen, wird ein OutOfMemoryError geworfen wird. Ich habe dieselbe Anwendung unter Windows ausgeführt und sehe, dass die Größe des virtuellen Speichers und die Heap-Größe ähnlich sind.

Gibt es eine Möglichkeit, den verwendeten virtuellen Speicher für einen Java-Prozess unter Linux zu konfigurieren?

Bearbeiten 1 : Das Problem ist nicht der Heap. Das Problem ist, dass, wenn ich einen Heap von 128 MB, zum Beispiel, immer noch Linux 210 MB virtuellen Speicher zuweist, die nicht benötigt wird, ever.**

Bearbeiten 2 : Verwendung von ulimit -v ermöglicht die Begrenzung der Menge des virtuellen Speichers. Wenn die eingestellte Größe unter 204 MB liegt, kann die Anwendung nicht ausgeführt werden, obwohl sie nicht 204 MB, sondern nur 64 MB benötigt. Ich möchte also verstehen, warum Java so viel virtuellen Speicher benötigt. Kann dies geändert werden?

Bearbeiten 3 : Auf dem eingebetteten System laufen mehrere andere Anwendungen. Und das System hat eine Begrenzung des virtuellen Speichers (aus Kommentaren, wichtiges Detail).

718voto

kdgregory Punkte 37614

Dies ist eine seit langem bestehende Beschwerde über Java, aber sie ist weitgehend bedeutungslos und basiert in der Regel auf der Betrachtung der falschen Informationen. Die übliche Formulierung ist so etwas wie "Hello World auf Java braucht 10 Megabyte! Warum braucht es das?" Nun, hier ist eine Möglichkeit, Hello World auf einer 64-Bit-JVM so zu gestalten, dass es über 4 Gigabyte benötigt ... zumindest nach einer Art von Messung.

java -Xms1024m -Xmx4096m com.example.Hello

Verschiedene Arten der Gedächtnismessung

Unter Linux wird die top gibt Ihnen verschiedene Zahlen für den Speicher an. Hier ist, was es über das Hello World Beispiel sagt:

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
 2120 kgregory  20   0 4373m  15m 7152 S    0  0.2   0:00.10 java
  • VIRT ist der virtuelle Speicherplatz: die Summe aller Daten in der virtuellen Speicherkarte (siehe unten). Er ist weitgehend bedeutungslos, außer wenn er es nicht ist (siehe unten).
  • RES ist die Größe des residenten Speichers: die Anzahl der Seiten, die sich derzeit im RAM befinden. In fast allen Fällen ist dies die einzige Zahl, die Sie verwenden sollten, wenn Sie "zu groß" sagen. Aber es ist immer noch keine sehr gute Zahl, besonders wenn es um Java geht.
  • SHR ist die Menge des residenten Speichers, die mit anderen Prozessen geteilt wird. Bei einem Java-Prozess beschränkt sich dies in der Regel auf gemeinsam genutzte Bibliotheken und im Speicher abgebildete JAR-Dateien. In diesem Beispiel lief nur ein Java-Prozess, so dass ich vermute, dass die 7k auf die vom Betriebssystem verwendeten Bibliotheken zurückzuführen sind.
  • SWAP ist standardmäßig nicht aktiviert und wird hier nicht angezeigt. Sie zeigt die Menge an virtuellem Speicher an, die sich derzeit auf der Festplatte befindet, ob es sich tatsächlich im Swap Space befindet oder nicht . Das Betriebssystem ist sehr gut darin, aktive Seiten im Arbeitsspeicher zu halten, und die einzigen Heilmittel für das Auslagern sind (1) mehr Arbeitsspeicher zu kaufen oder (2) die Anzahl der Prozesse zu reduzieren, daher ist es am besten, diese Zahl zu ignorieren.

Beim Windows Task Manager ist die Situation etwas komplizierter. Unter Windows XP gibt es die Spalten "Speichernutzung" und "Größe des virtuellen Speichers", aber die offizielle Dokumentation sagt nichts darüber aus, was sie bedeuten. Windows Vista und Windows 7 fügen mehr Spalten hinzu, und sie sind tatsächlich dokumentiert . Davon ist die Messung des "Working Set" am nützlichsten; sie entspricht ungefähr der Summe von RES und SHR unter Linux.

Die Abbildung des virtuellen Speichers verstehen

Der virtuelle Speicher, der von einem Prozess verbraucht wird, ist die Summe aller Daten, die sich in der Speicherabbildung des Prozesses befinden. Dazu gehören Daten (z. B. der Java-Heap), aber auch alle gemeinsam genutzten Bibliotheken und Dateien, die vom Programm im Speicher abgebildet werden. Unter Linux können Sie die pmap um alle Dinge zu sehen, die dem Prozessraum zugeordnet sind (von nun an werde ich mich nur noch auf Linux beziehen, weil ich es benutze; ich bin mir sicher, dass es entsprechende Tools für Windows gibt). Hier ist ein Auszug aus der Speicherabbildung des "Hello World"-Programms; die gesamte Speicherabbildung ist über 100 Zeilen lang, und es ist nicht ungewöhnlich, eine tausendzeilige Liste zu haben.

0000000040000000     36K r-x--  /usr/local/java/jdk-1.6-x64/bin/java
0000000040108000      8K rwx--  /usr/local/java/jdk-1.6-x64/bin/java
0000000040eba000    676K rwx--    \[ anon \]
00000006fae00000  21248K rwx--    \[ anon \]
00000006fc2c0000  62720K rwx--    \[ anon \]
0000000700000000 699072K rwx--    \[ anon \]
000000072aab0000 2097152K rwx--    \[ anon \]
00000007aaab0000 349504K rwx--    \[ anon \]
00000007c0000000 1048576K rwx--    \[ anon \]
...
00007fa1ed00d000   1652K r-xs-  /usr/local/java/jdk-1.6-x64/jre/lib/rt.jar
...
00007fa1ed1d3000   1024K rwx--    \[ anon \]
00007fa1ed2d3000      4K -----    \[ anon \]
00007fa1ed2d4000   1024K rwx--    \[ anon \]
00007fa1ed3d4000      4K -----    \[ anon \]
...
00007fa1f20d3000    164K r-x--  /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so
00007fa1f20fc000   1020K -----  /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so
00007fa1f21fb000     28K rwx--  /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so
...
00007fa1f34aa000   1576K r-x--  /lib/x86\_64-linux-gnu/libc-2.13.so
00007fa1f3634000   2044K -----  /lib/x86\_64-linux-gnu/libc-2.13.so
00007fa1f3833000     16K r-x--  /lib/x86\_64-linux-gnu/libc-2.13.so
00007fa1f3837000      4K rwx--  /lib/x86\_64-linux-gnu/libc-2.13.so
...

Eine kurze Erklärung des Formats: Jede Zeile beginnt mit der Adresse des virtuellen Speichers des Segments. Darauf folgen die Segmentgröße, die Berechtigungen und die Quelle des Segments. Das letzte Element ist entweder eine Datei oder "anon", was auf einen Speicherblock hinweist, der über mmap .

Von oben beginnend, haben wir

  • Der JVM-Lader (d.h. das Programm, das ausgeführt wird, wenn Sie java ). Diese ist sehr klein; sie lädt lediglich die gemeinsam genutzten Bibliotheken, in denen der eigentliche JVM-Code gespeichert ist.
  • Ein Haufen anonymer Blöcke, die den Java-Heap und interne Daten enthalten. Da es sich um eine JVM von Sun handelt, ist der Heap in mehrere Generationen unterteilt, von denen jede einen eigenen Speicherblock darstellt. Beachten Sie, dass die JVM den virtuellen Speicherplatz auf der Grundlage des -Xmx Wert; dies ermöglicht es, einen zusammenhängenden Heap zu haben. Die Website -Xms Wert wird intern verwendet, um zu sagen, wie viel des Heaps "in Gebrauch" ist, wenn das Programm startet, und um die Garbage Collection auszulösen, wenn sich diese Grenze nähert.
  • Eine memory-mapped JAR-Datei, in diesem Fall die Datei, die die "JDK-Klassen" enthält. Wenn Sie ein Memory-Mapping für eine JAR-Datei durchführen, können Sie sehr effizient auf die darin enthaltenen Dateien zugreifen (im Gegensatz zum Lesen der Datei von Anfang an). Die Sun JVM ordnet alle JARs auf dem Klassenpfad dem Speicher zu; wenn Ihr Anwendungscode auf ein JAR zugreifen muss, können Sie es ebenfalls dem Speicher zuordnen.
  • Pro-Thread-Daten für zwei Threads. Der 1M-Block ist der Thread-Stack. Ich hatte keine gute Erklärung für den 4k-Block, aber @ericsoe identifizierte ihn als "Guard-Block": Er hat keine Lese-/Schreibrechte und verursacht daher einen Segmentfehler, wenn auf ihn zugegriffen wird, und die JVM fängt das ab und übersetzt es in einen StackOverFlowError . Bei einer echten Anwendung werden Sie Dutzende, wenn nicht Hunderte dieser Einträge sehen, die sich in der Memory Map wiederholen.
  • Eine der gemeinsam genutzten Bibliotheken, die den eigentlichen JVM-Code enthält. Hiervon gibt es mehrere.
  • Die gemeinsam genutzte Bibliothek für die C-Standardbibliothek. Dies ist nur eines der vielen Dinge, die die JVM lädt und die nicht unbedingt zu Java gehören.

Die gemeinsam genutzten Bibliotheken sind besonders interessant: Jede gemeinsam genutzte Bibliothek hat mindestens zwei Segmente: ein Nur-Lese-Segment, das den Bibliothekscode enthält, und ein Lese-Schreib-Segment, das globale prozessspezifische Daten für die Bibliothek enthält (ich weiß nicht, was das Segment ohne Berechtigungen ist; ich habe es nur unter x64 Linux gesehen). Der schreibgeschützte Teil der Bibliothek kann von allen Prozessen, die die Bibliothek verwenden, gemeinsam genutzt werden; zum Beispiel, libc hat 1,5M virtuellen Speicherplatz, der gemeinsam genutzt werden kann.

Wann ist die Größe des virtuellen Speichers wichtig?

Die virtuelle Speicherkarte enthält eine Menge Dinge. Einiges davon ist schreibgeschützt, einiges wird gemeinsam genutzt, und einiges wird zugewiesen, aber nie berührt (z.B. fast die gesamten 4 GB des Heaps in diesem Beispiel). Aber das Betriebssystem ist klug genug, nur das zu laden, was es braucht, so dass die Größe des virtuellen Speichers weitgehend irrelevant ist.

Die Größe des virtuellen Speichers ist wichtig, wenn Sie mit einem 32-Bit-Betriebssystem arbeiten, bei dem Sie nur 2 GB (oder in einigen Fällen 3 GB) Prozessadressraum zuweisen können. In diesem Fall haben Sie es mit einer knappen Ressource zu tun und müssen möglicherweise Kompromisse eingehen, z. B. die Heap-Größe reduzieren, um eine große Datei im Speicher abzubilden oder viele Threads zu erstellen.

Da aber 64-Bit-Maschinen allgegenwärtig sind, wird es wohl nicht mehr lange dauern, bis die Größe des virtuellen Speichers eine völlig irrelevante Statistik ist.

Wann ist die Größe des Residenten-Sets wichtig?

Die Resident Set-Größe ist der Teil des virtuellen Speicherplatzes, der sich tatsächlich im RAM befindet. Wenn Ihr RSS einen beträchtlichen Teil des gesamten physischen Speichers einnimmt, ist es vielleicht an der Zeit, sich Sorgen zu machen. Wenn Ihr RSS den gesamten physischen Speicher beansprucht und Ihr System anfängt zu swappen, ist es längst an der Zeit, sich Sorgen zu machen.

Aber RSS ist auch irreführend, besonders auf einem leicht belasteten Rechner. Das Betriebssystem betreibt keinen großen Aufwand, um die von einem Prozess belegten Seiten zurückzufordern. Der Nutzen ist gering, und es besteht die Gefahr eines teuren Seitenfehlers, wenn der Prozess die Seite in Zukunft berührt. Infolgedessen kann die RSS-Statistik viele Seiten enthalten, die nicht aktiv genutzt werden.

Unterm Strich

Solange Sie nicht auslagern, sollten Sie sich nicht zu viele Gedanken darüber machen, was die verschiedenen Speicherstatistiken Ihnen sagen. Mit dem Vorbehalt, dass eine ständig wachsende RSS auf eine Art von Speicherleck hinweisen kann.

Bei einem Java-Programm ist es viel wichtiger, darauf zu achten, was im Heap passiert. Die Gesamtmenge des verbrauchten Platzes ist wichtig, und es gibt einige Schritte, die Sie unternehmen können, um dies zu reduzieren. Wichtiger ist die Zeit, die Sie mit der Garbage Collection verbringen, und welche Teile des Heaps gesammelt werden.

Der Zugriff auf die Festplatte (z. B. eine Datenbank) ist teuer, der Arbeitsspeicher ist billig. Wenn Sie das eine gegen das andere tauschen können, sollten Sie das tun.

52voto

Lari Hotari Punkte 4730

Es gibt ein bekanntes Problem mit Java und glibc >= 2.10 (einschließlich Ubuntu >= 10.04, RHEL >= 6).

Die Abhilfe besteht darin, diese Umgebungsvariable zu setzen:

export MALLOC_ARENA_MAX=4

Wenn Sie Tomcat verwenden, können Sie dies zu TOMCAT_HOME/bin/setenv.sh Datei.

Für Docker, fügen Sie dies zu Dockerfile hinzu

ENV MALLOC_ARENA_MAX=4

Es gibt einen IBM-Artikel über die Einstellung von MALLOC_ARENA_MAX https://www.ibm.com/developerworks/community/blogs/kevgrig/entry/linux_glibc_2_10_rhel_6_malloc_may_show_excessive_virtual_memory_usage?lang=en

In diesem Blogbeitrag heißt es

Der residente Speicher ist dafür bekannt, dass er sich ähnlich wie ein Speicherleck oder Speicherfragmentierung.

Es gibt auch einen offenen JDK-Fehler JDK-8193521 "glibc verschwendet Speicher mit Standardkonfiguration"

Suche nach MALLOC_ARENA_MAX auf Google oder SO für weitere Referenzen.

Möglicherweise möchten Sie auch andere malloc-Optionen einstellen, um eine geringe Fragmentierung des zugewiesenen Speichers zu erreichen:

# tune glibc memory allocation, optimize for low fragmentation
# limit the number of arenas
export MALLOC_ARENA_MAX=2
# disable dynamic mmap threshold, see M_MMAP_THRESHOLD in "man mallopt"
export MALLOC_MMAP_THRESHOLD_=131072
export MALLOC_TRIM_THRESHOLD_=131072
export MALLOC_TOP_PAD_=131072
export MALLOC_MMAP_MAX_=65536

11voto

James Schek Punkte 17598

Der für den Java-Prozess zugewiesene Speicherplatz entspricht ziemlich genau dem, was ich erwarten würde. Ich hatte ähnliche Probleme bei der Ausführung von Java auf eingebetteten Systemen mit begrenztem Speicherplatz. Ausführen von jede Anwendungen mit willkürlichen VM-Limits oder auf Systemen, die nicht über ausreichend Swap-Speicher verfügen, neigen dazu, abzubrechen. Das scheint die Natur vieler moderner Anwendungen zu sein, die nicht für die Verwendung auf ressourcenbeschränkten Systemen konzipiert sind.

Es gibt noch ein paar weitere Optionen, die Sie ausprobieren können, um den Speicherbedarf Ihrer JVM zu begrenzen. Dies könnte den virtuellen Speicherbedarf verringern:

-XX:ReservedCodeCacheSize=32m Reservierte Code-Cache-Größe (in Bytes) - maximale Code-Cache-Größe. [Solaris 64-Bit, amd64, und -server x86: 48m; in 1.5.0_06 und früher, Solaris 64-Bit und and64: 1024m].

-XX:MaxPermSize=64m Größe der permanenten Generation. [5.0 und neuere Versionen: 64-Bit-VMs werden um 30% größer skaliert; 1.4 amd64: 96m; 1.3.1 -client: 32m.]

Außerdem sollten Sie Ihre -Xmx (maximale Heap-Größe) auf einen Wert setzen, der so nah wie möglich an der tatsächlicher Spitzen-Speicherverbrauch Ihrer Bewerbung. Ich glaube, das Standardverhalten der JVM ist immer noch double die Heap-Größe jedes Mal bis zur Maximalgröße erweitert. Wenn Sie mit 32M Heap beginnen und Ihre App 65M erreicht, dann würde der Heap am Ende 32M -> 64M -> 128M wachsen.

Sie können dies auch versuchen, um die VM weniger aggressiv beim Wachsen des Heaps zu machen:

-XX:MinHeapFreeRatio=40 Mindestprozentsatz des nach der GC freien Heaps, um Expansion zu vermeiden.

Soweit ich mich erinnere, hatte die Anzahl der geladenen nativen Bibliotheken einen großen Einfluss auf den minimalen Speicherplatzbedarf, als ich vor einigen Jahren damit experimentierte. Das Laden von java.net.Socket fügte mehr als 15M hinzu, wenn ich mich richtig erinnere (und wahrscheinlich täusche ich mich).

8voto

Die Sun JVM benötigt viel Speicher für HotSpot und bildet die Laufzeitbibliotheken im gemeinsamen Speicher ab.

Wenn der Speicherplatz ein Problem darstellt, sollten Sie eine andere für die Einbettung geeignete JVM verwenden. IBM hat j9, und es gibt die Open Source "jamvm", die GNU Klassenpfad-Bibliotheken verwendet. Auch Sun hat die Squeak JVM, die auf dem SunSPOTS läuft, also gibt es Alternativen.

5voto

runholen Punkte 750

Eine Möglichkeit, den Heap eines Systems mit begrenzten Ressourcen zu verkleinern, kann darin bestehen, mit der Variable -XX:MaxHeapFreeRatio zu spielen. Diese ist normalerweise auf 70 gesetzt und ist der maximale Prozentsatz des Heaps, der frei ist, bevor der GC ihn verkleinert. Wenn Sie diese Variable auf einen niedrigeren Wert setzen, werden Sie z.B. im jvisualvm Profiler sehen, dass ein kleinerer Heap-Size für Ihr Programm verwendet wird.

EDIT: Um kleine Werte für -XX:MaxHeapFreeRatio zu setzen, müssen Sie auch -XX:MinHeapFreeRatio setzen Zum Beispiel

java -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=25 HelloWorld

EDIT2: Ich habe ein Beispiel für eine echte Anwendung hinzugefügt, die die gleiche Aufgabe startet und ausführt, einmal mit Standardparametern und einmal mit 10 und 25 als Parameter. Ich konnte keinen wirklichen Geschwindigkeitsunterschied feststellen, obwohl Java im letzteren Beispiel theoretisch mehr Zeit für die Vergrößerung des Heaps benötigen sollte.

Default parameters

Am Ende beträgt der maximale Heap 905, der verwendete Heap 378

MinHeap 10, MaxHeap 25

Am Ende beträgt der maximale Heap 722, der verwendete Heap 378.

Dies hat tatsächlich einige Auswirkungen, da unsere Anwendung auf einem Remote-Desktop-Server läuft und von vielen Benutzern gleichzeitig ausgeführt werden kann.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X