GNU/Linux >> Linux Esercitazione >  >> Linux

Utilizzo della memoria virtuale da Java sotto Linux, troppa memoria utilizzata

Questa è stata una lamentela di lunga data con Java, ma è in gran parte priva di significato e di solito si basa sull'osservazione di informazioni sbagliate. Il solito fraseggio è qualcosa del tipo "Hello World su Java richiede 10 megabyte! Perché ne ha bisogno?" Bene, ecco un modo per fare in modo che Hello World su una JVM a 64 bit dichiari di occupare più di 4 gigabyte... almeno secondo una forma di misurazione.

java -Xms1024m -Xmx4096m com.example.Hello

Diversi modi per misurare la memoria

Su Linux, il comando top fornisce diversi numeri per la memoria. Ecco cosa dice dell'esempio Hello World:

  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 è lo spazio di memoria virtuale:la somma di tutto nella mappa di memoria virtuale (vedi sotto). È in gran parte privo di significato, tranne quando non lo è (vedi sotto).
  • RES è la dimensione del set residente:il numero di pagine che sono attualmente residenti nella RAM. In quasi tutti i casi, questo è l'unico numero che dovresti usare quando dici "troppo grande". Ma non è ancora un buon numero, soprattutto quando si parla di Java.
  • SHR è la quantità di memoria residente condivisa con altri processi. Per un processo Java, questo è in genere limitato alle librerie condivise e ai file JAR mappati in memoria. In questo esempio, avevo un solo processo Java in esecuzione, quindi sospetto che il 7k sia il risultato delle librerie utilizzate dal sistema operativo.
  • SWAP non è attivato per impostazione predefinita e non viene mostrato qui. Indica la quantità di memoria virtuale attualmente residente su disco, indipendentemente dal fatto che si trovi o meno nello spazio di swap . Il sistema operativo è molto bravo a mantenere le pagine attive nella RAM e le uniche cure per lo scambio sono (1) acquistare più memoria o (2) ridurre il numero di processi, quindi è meglio ignorare questo numero.

La situazione per Windows Task Manager è un po' più complicata. Sotto Windows XP, ci sono le colonne "Utilizzo memoria" e "Dimensione memoria virtuale", ma la documentazione ufficiale tace sul loro significato. Windows Vista e Windows 7 aggiungono più colonne e sono effettivamente documentate. Di questi, la misura "Working Set" è la più utile; corrisponde approssimativamente alla somma di RES e SHR su Linux.

Capire la mappa della memoria virtuale

La memoria virtuale consumata da un processo è il totale di tutto ciò che è nella mappa della memoria del processo. Ciò include i dati (ad esempio, l'heap Java), ma anche tutte le librerie condivise ei file mappati in memoria utilizzati dal programma. Su Linux, puoi usare il comando pmap per vedere tutte le cose mappate nello spazio del processo (da qui in poi mi riferirò solo a Linux, perché è quello che uso; sono sicuro che ci sono strumenti equivalenti per Finestre). Ecco un estratto dalla mappa della memoria del programma "Hello World"; l'intera mappa della memoria è lunga più di 100 righe e non è insolito avere un elenco di mille righe.

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

Una rapida spiegazione del formato:ogni riga inizia con l'indirizzo di memoria virtuale del segmento. Questo è seguito dalla dimensione del segmento, dalle autorizzazioni e dall'origine del segmento. Quest'ultimo elemento è un file o "anon", che indica un blocco di memoria allocato tramite mmap.

Partendo dall'alto, abbiamo

  • Il JVM loader (ovvero il programma che viene eseguito quando digiti java ). Questo è molto piccolo; tutto ciò che fa è caricare nelle librerie condivise dove è memorizzato il vero codice JVM.
  • Un mucchio di blocchi anonimi che contengono l'heap Java e i dati interni. Questa è una Sun JVM, quindi l'heap è suddiviso in più generazioni, ognuna delle quali è il proprio blocco di memoria. Si noti che la JVM alloca lo spazio di memoria virtuale in base a -Xmx valore; questo gli permette di avere un heap contiguo. Il -Xms value è usato internamente per dire quanta parte dell'heap è "in uso" all'avvio del programma e per attivare la raccolta dei rifiuti quando ci si avvicina a quel limite.
  • Un file JAR mappato in memoria, in questo caso il file che contiene le "classi JDK". Quando esegui la mappatura della memoria di un JAR, puoi accedere ai file al suo interno in modo molto efficiente (rispetto a leggerlo dall'inizio ogni volta). Sun JVM eseguirà la mappatura della memoria di tutti i JAR sul classpath; se il codice della tua applicazione deve accedere a un JAR, puoi anche mapparlo in memoria.
  • Dati per thread per due thread. Il blocco 1M è lo stack di thread. Non avevo una buona spiegazione per il blocco 4k, ma @ericsoe lo ha identificato come un "blocco di guardia":non ha permessi di lettura/scrittura, quindi causerà un errore di segmento se vi si accede e la JVM lo rileva e lo traduce a un StackOverFlowError . Per una vera app, vedrai dozzine se non centinaia di queste voci ripetute attraverso la mappa della memoria.
  • Una delle librerie condivise che contiene il codice JVM effettivo. Ce ne sono diversi.
  • La libreria condivisa per la libreria standard C. Questa è solo una delle tante cose caricate dalla JVM che non fanno strettamente parte di Java.

Le librerie condivise sono particolarmente interessanti:ogni libreria condivisa ha almeno due segmenti:un segmento di sola lettura contenente il codice della libreria e un segmento di lettura-scrittura che contiene dati globali per processo per la libreria (non so quale segmento senza autorizzazioni è; l'ho visto solo su x64 Linux). La porzione di sola lettura della libreria può essere condivisa tra tutti i processi che utilizzano la libreria; ad esempio, libc dispone di 1,5 M di spazio di memoria virtuale che può essere condiviso.

Quando è importante la dimensione della memoria virtuale?

La mappa della memoria virtuale contiene molte cose. Parte è di sola lettura, parte è condivisa e parte è allocata ma mai toccata (ad esempio, quasi tutti i 4 Gb di heap in questo esempio). Ma il sistema operativo è abbastanza intelligente da caricare solo ciò di cui ha bisogno, quindi la dimensione della memoria virtuale è in gran parte irrilevante.

Dove la dimensione della memoria virtuale è importante è se si esegue su un sistema operativo a 32 bit, in cui è possibile allocare solo 2 Gb (o, in alcuni casi, 3 Gb) di spazio degli indirizzi del processo. In tal caso hai a che fare con una risorsa scarsa e potresti dover fare dei compromessi, come ridurre la dimensione dell'heap per mappare in memoria un file di grandi dimensioni o creare molti thread.

Ma, dato che le macchine a 64 bit sono onnipresenti, non credo che passerà molto tempo prima che la dimensione della memoria virtuale diventi una statistica completamente irrilevante.

Quando è importante la dimensione del set di residenti?

La dimensione del set residente è quella parte dello spazio di memoria virtuale che si trova effettivamente nella RAM. Se il tuo RSS diventa una parte significativa della tua memoria fisica totale, potrebbe essere il momento di iniziare a preoccuparti. Se il tuo RSS cresce fino a occupare tutta la tua memoria fisica e il tuo sistema inizia a eseguire lo scambio, è ormai passato il tempo di iniziare a preoccuparti.

Ma RSS è anche fuorviante, specialmente su una macchina poco caricata. Il sistema operativo non impiega molti sforzi per recuperare le pagine utilizzate da un processo. C'è poco vantaggio da ottenere in questo modo e il potenziale per un costoso errore di pagina se il processo tocca la pagina in futuro. Di conseguenza, la statistica RSS può includere molte pagine che non sono in uso attivo.

Risultato finale

A meno che tu non stia scambiando, non preoccuparti eccessivamente di ciò che ti dicono le varie statistiche sulla memoria. Con l'avvertenza che un RSS in continua crescita potrebbe indicare una sorta di perdita di memoria.

Con un programma Java, è molto più importante prestare attenzione a ciò che accade nell'heap. La quantità totale di spazio consumato è importante e ci sono alcuni passaggi che puoi adottare per ridurla. Più importante è la quantità di tempo che dedichi alla raccolta dei rifiuti e quali parti dell'heap vengono raccolte.

L'accesso al disco (ad esempio, un database) è costoso e la memoria è economica. Se puoi scambiare l'uno con l'altro, fallo.


La quantità di memoria allocata per il processo Java è praticamente alla pari con quello che mi aspetterei. Ho avuto problemi simili con l'esecuzione di Java su sistemi embedded/con memoria limitata. Esecuzione di any le applicazioni con limiti VM arbitrari o su sistemi che non dispongono di quantità adeguate di swap tendono a rompersi. Sembra essere la natura di molte app moderne che non sono progettate per l'uso su sistemi con risorse limitate.

Hai alcune altre opzioni che puoi provare e limitare il footprint di memoria della tua JVM. Ciò potrebbe ridurre l'impronta di memoria virtuale:

-XX:ReservedCodeCacheSize=32m Dimensione della cache del codice riservata (in byte) - dimensione massima della cache del codice. [Solaris 64 bit, amd64 e -server x86:48 m; in1.5.0_06 e versioni precedenti, Solaris a 64 bit e and64:1024 milioni.]

-XX:MaxPermSize=64m Dimensione della generazione permanente. [5.0 e versioni successive:le macchine virtuali a 64 bit vengono ridimensionate del 30% in più; 1.4amd64:96m; 1.3.1 -client:32m.]

Inoltre, dovresti anche impostare -Xmx (dimensione massima dell'heap) su un valore il più vicino possibile al massimo utilizzo effettivo della memoria della tua candidatura. Credo che il comportamento predefinito della JVM sia ancora quello di raddoppiare la dimensione dell'heap ogni volta che lo espande fino al massimo. Se inizi con un heap di 32 milioni e la tua app raggiunge un picco di 65 milioni, l'heap finirà per crescere di 32 milioni -> 64 milioni -> 128 milioni.

Potresti anche provare questo per rendere la VM meno aggressiva riguardo alla crescita dell'heap:

-XX:MinHeapFreeRatio=40 Percentuale minima di heap libero dopo GC per evitare l'espansione.

Inoltre, da quello che ricordo di aver sperimentato questo alcuni anni fa, il numero di librerie native caricate ha avuto un enorme impatto sull'ingombro minimo. Il caricamento di java.net.Socket ha aggiunto più di 15M se ricordo bene (e probabilmente non lo ricordo).


Esiste un problema noto con Java e glibc>=2.10 (include Ubuntu>=10.04, RHEL>=6).

La cura è impostare questo env. variabile:

export MALLOC_ARENA_MAX=4

Se stai utilizzando Tomcat, puoi aggiungerlo a TOMCAT_HOME/bin/setenv.sh file.

Per Docker, aggiungilo a Dockerfile

ENV MALLOC_ARENA_MAX=4

C'è un articolo IBM sull'impostazione di MALLOC_ARENA_MAXhttps://www.ibm.com/developerworks/community/blogs/kevgrig/entry/linux_glibc_2_10_rhel_6_malloc_may_show_excessive_virtual_memory_usage?lang=en

Questo post sul blog dice

è noto che la memoria residente si insinua in modo simile alla perdita di memoria o alla frammentazione della memoria.

C'è anche un bug JDK aperto JDK-8193521 "glibc spreca memoria con la configurazione predefinita"

cerca MALLOC_ARENA_MAX su Google o SO per ulteriori riferimenti.

Potresti voler mettere a punto anche altre opzioni malloc per ottimizzare la bassa frammentazione della memoria allocata:

# 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

Linux
  1. Installa Java dai repository della tua distribuzione Linux

  2. Linux:limite all'utilizzo della memoria per un singolo processo Linux?

  3. Linux:determinare correttamente l'utilizzo della memoria in Linux?

  4. Come controllare l'utilizzo della memoria in un server basato su Linux

  5. Picco di utilizzo della memoria di un processo Linux/Unix

Utilizzo della memoria di Linux

Come controllare l'utilizzo della memoria in Linux

Comando gratuito Linux (controlla l'utilizzo della memoria)

Come impedire a PHP-FPM di consumare troppa RAM in Linux

Gestione della memoria Linux:memoria virtuale e paginazione della domanda

Determinazione corretta dell'utilizzo della memoria in Linux