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

Ho un problema con un’applicazione Java in esecuzione su Linux.

Quando lancio l’applicazione, utilizzando la dimensione massima dell’heap (64 MB), vedo usando l’applicazione tops che 240 MB di memoria virtuale sono allocati all’applicazione. Ciò crea alcuni problemi con alcuni altri software sul computer, che è relativamente limitato in termini di risorse.

La memoria virtuale riservata non verrà comunque utilizzata, per quanto ho capito, perché una volta raggiunto il limite di heap viene generato un OutOfMemoryError . Ho eseguito la stessa applicazione sotto Windows e vedo che la dimensione della memoria virtuale e la dimensione dell’heap sono simili.

Esiste comunque la possibilità di configurare la memoria virtuale in uso per un processo Java sotto Linux?

Modifica 1 : il problema non è l’heap. Il problema è che se imposto un Heap di 128 MB, ad esempio, Linux assegna ancora 210 MB di memoria virtuale, che non è necessario, mai. **

Modifica 2 : l’uso di ulimit -v consente di limitare la quantità di memoria virtuale. Se la dimensione impostata è inferiore a 204 MB, l’applicazione non verrà eseguita anche se non ha bisogno di 204 MB, solo 64 MB. Quindi voglio capire perché Java richiede tanta memoria virtuale. Questo può essere cambiato?

Modifica 3 : ci sono molte altre applicazioni in esecuzione nel sistema, che è incorporato. E il sistema ha un limite di memoria virtuale (dai commenti, dettagli importanti).

Questa è stata una lamencanvas di vecchia data con Java, ma è in gran parte priva di significato e di solito si basa sull’osservazione delle informazioni sbagliate. Il solito fraseggio è qualcosa come “Hello World on Java richiede 10 megabyte! Perché ne ha bisogno?” Bene, ecco un modo per rendere Hello World su una rivendicazione JVM a 64 bit di acquisire oltre 4 gigabyte … almeno con una forma di misurazione.

 java -Xms1024m -Xmx4096m com.example.Hello

Modi diversi per misurare la memoria

Su Linux, il comando superiore ti dà 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 della memoria virtuale: la sum di tutto nella mappa della memoria virtuale (vedi sotto). È in gran parte privo di significato, tranne quando non lo è (vedi sotto).
  • RES è la dimensione impostata residente: il numero di pagine che sono attualmente residenti nella RAM. In quasi tutti i casi, questo è l’unico numero da utilizzare quando si dice “troppo grande”. Ma non è ancora un numero molto buono, specialmente 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, ho avuto solo un processo Java in esecuzione, quindi sospetto che il 7k sia il risultato delle librerie utilizzate dal sistema operativo.
  • SWAP non è triggersto di default e non è mostrato qui. Indica la quantità di memoria virtuale attualmente residente sul disco, indipendentemente dal fatto che si trovi effettivamente nello spazio di scambio . Il sistema operativo è ottimo per mantenere attive le pagine 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 Task Manager di Windows è un po ‘più complicata. Sotto Windows XP, ci sono le colonne “Uso memoria” e “Dimensione memoria virtuale”, ma la documentazione ufficiale non dice cosa significano. Windows Vista e Windows 7 aggiungono più colonne e sono effettivamente documentati . Di questi, la misura “Working Set” è la più utile; corrisponde approssimativamente alla sum 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 di processo. Ciò include dati (ad esempio, l’heap Java), ma anche tutte le librerie condivise e file mappati in memoria usati dal programma. Su Linux, puoi usare il comando pmap per vedere tutte le cose mappate nello spazio del processo (da qui in avanti mi riferirò 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 migliaia di righe.

 0000000040000000 36K rx-- /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 rx-- /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 rx-- /lib/x86_64-linux-gnu/libc-2.13.so
 00007fa1f3634000 2044K ----- /lib/x86_64-linux-gnu/libc-2.13.so
 00007fa1f3833000 16K rx-- /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 dalla sorgente del segmento. Quest’ultimo elemento è un file o “anon”, che indica un blocco di memoria assegnato tramite mmap .

Partendo dall’alto, abbiamo

  • Il caricatore JVM (ovvero, il programma che viene eseguito quando si digita java ). Questo è molto piccolo; tutto ciò che fa è caricare nelle librerie condivise in cui è memorizzato il codice JVM reale.
  • Un mucchio di blocchi anon che contengono l’heap Java e i dati interni. Questa è una JVM Sun, 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 basato sul valore -Xmx ; questo gli permette di avere un heap contiguo. Il valore -Xms viene utilizzato internamente per indicare quanta parte dell’heap è “in uso” all’avvio del programma e per triggersre la garbage collection quando viene raggiunto tale limite.
  • Un file JAR mappato in memoria, in questo caso il file che contiene le “classi JDK”. Quando si memorizza la memoria di un JAR, è ansible accedere ai file al suo interno in modo molto efficiente (rispetto alla lettura dall’inizio ogni volta). The Sun JVM memorizzerà in memoria tutti i JAR sul classpath; se il codice dell’applicazione deve accedere a un JAR, è ansible anche mapparlo a memoria.
  • Dati per-thread per due thread. Il blocco 1M è una pila di thread; Non so cosa vada nel blocco 4K. Per una vera app, vedrai decine 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 molti di questi.
  • La libreria condivisa per la libreria standard C. Questa è solo una delle molte cose che carica la JVM che non sono 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 e scrittura che contiene dati globali per processo per la libreria (non so cosa sia il il segmento senza permessi è: l’ho visto solo su x64 Linux). La parte di sola lettura della libreria può essere condivisa tra tutti i processi che utilizzano la libreria; per esempio, libc ha 1.5M di spazio di memoria virtuale che può essere condiviso.

Quando è importante la dimensione della memoria virtuale?

La mappa della memoria virtuale contiene un sacco di cose. Alcune sono di sola lettura, alcune sono condivise e alcune sono allocate ma mai toccate (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 sta utilizzando un sistema operativo a 32 bit, in cui è ansible allocare solo 2 Gb (o, in alcuni casi, 3 Gb) di spazio di indirizzamento del processo. In questo caso hai a che fare con una risorsa scarsa e potresti dover fare dei compromessi, come ridurre la dimensione dell’heap in memoria, mappare un file di grandi dimensioni o creare molti thread.

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

Quando è importante impostare la dimensione del residente?

Resident Set size è 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 per occupare tutta la tua memoria fisica e il tuo sistema inizia a scambiare, è il momento giusto per iniziare a preoccuparti.

Ma RSS è anche fuorviante, specialmente su una macchina leggermente caricata. Il sistema operativo non impiega molto sforzo per recuperare le pagine utilizzate da un processo. In questo modo si ottengono pochi vantaggi e il potenziale di 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.

Linea di fondo

A meno che non si stia scambiando, non preoccuparsi eccessivamente di ciò che le varie statistiche sulla memoria ti dicono. Con l’avvertenza che un RSS in continua crescita può indicare una qualche 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 è ansible adottare per ridurlo. Più importante è la quantità di tempo che trascorri nella raccolta dei rifiuti e quali parti dell’heap vengono raccolte.

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

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

La cura è di impostare questo env. variabile: export MALLOC_ARENA_MAX=4 Se stai utilizzando Tomcat, puoi aggiungerlo al file TOMCAT_HOME/bin/setenv.sh .

Esiste un articolo IBM sull’impostazione di 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

Questo post del blog dice

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

cerca MALLOC_ARENA_MAX su Google o SO per ulteriori riferimenti.

Potresti voler ottimizzare anche altre opzioni di malloc per ottimizzare la frammentazione bassa 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 

La quantità di memoria allocata per il processo Java è praticamente in linea con quello che mi aspetterei. Ho avuto problemi simili con Java su sistemi embedded / memory limitati. L’esecuzione di qualsiasi applicazione con limiti arbitrari di VM o su sistemi che non hanno una quantità adeguata di swap tendono a interrompersi. Sembra essere la natura di molte app moderne che non sono progettate per l’uso su sistemi a risorse limitate.

Hai altre opzioni che puoi provare e limitare l’ingombro di memoria della tua JVM. Ciò potrebbe ridurre l’impronta di memoria virtuale:

-XX: ReservedCodeCacheSize = 32m Dimensioni della cache del codice riservato (in byte) – dimensione massima della cache del codice. [Solaris 64-bit, amd64 e -server x86: 48m; in 1.5.0_06 e precedenti, Solaris 64-bit e and64: 1024m.]

-XX: MaxPermSize = 64m Dimensione della generazione permanente. [5.0 e successivi: le macchine virtuali a 64 bit sono ridimensionate del 30%; 1,4 amd64: 96m; 1.3.1 -client: 32m.]

Inoltre, dovresti anche impostare il tuo -Xmx (dimensione massima dell’heap) su un valore il più vicino ansible all’effettivo utilizzo della memoria di picco della tua applicazione. Credo che il comportamento predefinito della JVM debba ancora raddoppiare le dimensioni dell’heap ogni volta che si espande al massimo. Se si inizia con l’heap 32M e l’app raggiunge il limite massimo di 65M, l’heap finisce per crescere 32 M -> 64 M -> 128 M.

Si potrebbe anche provare questo per rendere la VM meno aggressiva sulla crescita dell’heap:

-XX: MinHeapFreeRatio = 40 Minima percentuale di heap libero dopo GC per evitare l’espansione.

Inoltre, da quello che ricordo di aver sperimentato con questo alcuni anni fa, il numero di librerie native caricate ha avuto un impatto enorme sull’impronta minima. Caricamento java.net.Socket ha aggiunto più di 15M se ricordo correttamente (e probabilmente non lo faccio).

Sun JVM richiede molta memoria per HotSpot e si associa alle librerie di runtime nella memoria condivisa.

Se la memoria è un problema, considera l’utilizzo di un’altra JVM adatta per l’incorporamento. IBM ha j9, e c’è l’open source “jamvm” che usa le librerie del classpath GNU. Anche Sun ha Squeak JVM in esecuzione su SunSPOTS, quindi ci sono alternative.

Solo un pensiero, ma puoi controllare l’influenza di un’opzione ulimit -v .

Questa non è una soluzione reale dal momento che limiterebbe lo spazio degli indirizzi disponibile per tutti i processi, ma ciò consentirebbe di controllare il comportamento dell’applicazione con una memoria virtuale limitata.

Un modo per ridurre l’heap di un sistema con risorse limitate potrebbe essere quello di giocare con la variabile -XX: MaxHeapFreeRatio. Di solito è impostato su 70, ed è la percentuale massima dell’heap che è gratuita prima che il GC lo ritiri. Impostandolo su un valore più basso, vedrai ad esempio il profiler jvisualvm che un programma di heap più piccolo viene solitamente usato per il tuo programma.

EDIT: per impostare valori piccoli per -XX: MaxHeapFreeRatio è necessario impostare anche -XX: MinHeapFreeRatio Es.

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

EDIT2: Aggiunto un esempio per un’applicazione reale che avvia e fa lo stesso compito, uno con i parametri predefiniti e uno con 10 e 25 come parametri. Non ho notato alcuna reale differenza di velocità, anche se in teoria Java dovrebbe usare più tempo per aumentare l’heap nel secondo esempio.

Parametri di default

Alla fine, l’heap massimo è 905, l’heap utilizzato è 378

MinHeap 10, MaxHeap 25

Alla fine, l’heap massimo è 722, l’heap utilizzato è 378

Questo in realtà ha un po ‘di inpossibilità, dato che la nostra applicazione gira su un server desktop remoto e molti utenti possono eseguirlo contemporaneamente.

Sun’s Java 1.4 ha i seguenti argomenti per controllare la dimensione della memoria:

-Xmsn Specifica la dimensione iniziale, in byte, del pool di allocazione di memoria. Questo valore deve essere un multiplo di 1024 maggiore di 1 MB. Aggiungi la lettera k o K per indicare i kilobyte o m o M per indicare i megabyte. Il valore predefinito è 2 MB. Esempi:

  -Xms6291456 -Xms6144k -Xms6m 

-Xmxn Specifica la dimensione massima, in byte, del pool di allocazione di memoria. Questo valore deve essere un multiplo di 1024 maggiore di 2 MB. Aggiungi la lettera k o K per indicare i kilobyte o m o M per indicare i megabyte. Il valore predefinito è 64 MB. Esempi:

  -Xmx83886080 -Xmx81920k -Xmx80m 

http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/java.html

Java 5 e 6 ne hanno ancora. Vedi http://java.sun.com/javase/technologies/hotspot/vmoptions.jsp

No, non è ansible configurare la quantità di memoria necessaria per VM. Tuttavia, si noti che questa è memoria virtuale, non residente, quindi rimane semplicemente lì senza danno se non effettivamente utilizzata.

In alternativa, puoi provare qualche altra JVM e poi una Sun, con un ingombro di memoria minore, ma non posso consigliarti qui.