Heap, Stack & Friends
Quando il programma di cui ci occupiamo hai dei problemi relativi alla memoria, il primo tentativo spesso è quello di aumentarla. Ovviamente questa non è la cura giusta per ogni tipo di problema, quindi prima di aumentare la memoria fornita alla JVM dovremmo capire come effettivamente è organizzata internamente, per poi capire quale sia il modo giusto per intervenire.
Memoria JVM
La memoria che viene gestita dalla Java Virtual Machine è composta da diverse aree che possono essere riassunte dalla seguente immagine
La prima distinzione è quella relativa alle aree di memoria Managed e Native. La prima è l’area di memoria gestita direttamente dalla JVM, che quindi può usufruire del Garbage Collector mentre l’area Native racchiude una serie di aree che sono a più stretto contatto con il sistema operativo sottostante e che possono anche essere utilizzate direttamente da un programma.
La possibilità di utilizzare la memoria nativa può essere in alcune particolari situazioni, attraverso l’utilizzo della classe sun.misc.Unsafe e utilizzando il parametro -XX:MaxDirectMemorySize che permette di specificare il quantitativo massimo di memoria da poter utilizzare per Direct Byte Buffer.
Già il fatto che la classe si chiama Unsafe dovrebbe far venire in mente allo sviluppatore che forse prima di utilizzarla bisognerebbe pensarci bene, ma nella maggior parte dei casi nessuno sviluppatore utilizza direttamente questa API che permette di allocare e deallocare memoria come nel seguente esempio
package com.javastaff.memory; import java.lang.reflect.Field; import sun.misc.Unsafe; public class UnsafeTest { public static void main(String[] args) throws Exception { long sum = 0; long SUPER_SIZE = (long)Integer.MAX_VALUE * 2; SuperArray array = new SuperArray(SUPER_SIZE); System.out.println("Array size:" + array.size()); // 4294967294 for (int i = 0; i < 100; i++) { array.set((long)Integer.MAX_VALUE + i, (byte)3); sum += array.get((long)Integer.MAX_VALUE + i); } System.out.println("Somma:" + sum); } public static Unsafe getUnsafe() throws Exception { //Internal reference Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); Unsafe unsafe = (Unsafe) f.get(null); return unsafe; } } class SuperArray { private final static int BYTE = 1; private long size; private long address; public SuperArray(long size) throws Exception { this.size = size; address = UnsafeTest.getUnsafe().allocateMemory(size * BYTE); } public void set(long i, byte value) throws Exception { UnsafeTest.getUnsafe().putByte(address + i * BYTE, value); } public int get(long idx) throws Exception { return UnsafeTest.getUnsafe().getByte(address + idx * BYTE); } public long size() { return size; } }
In questo esempio si utilizza Unsafe per allocare un array di byte con dimensione doppia rispetto al massimo consentito (4294967294). Proprio per questo motivo lanciando questo programma potreste avere il seguente risultato
Array size:4294967294 # # A fatal error has been detected by the Java Runtime Environment: # # EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x52dd2376, pid=3492, tid=4568 # # JRE version: 7.0_25-b16 # Java VM: Java HotSpot(TM) Client VM (23.25-b01 mixed mode, sharing windows-x86 ) # Problematic frame: # V [jvm.dll+0x112376]
In alcune situazioni limite può essere l’utilizzo della memoria nativa, infatti c’è anche la proposta di creare un API pubblica in Java 9. Per quanto riguarda invece la memoria Managed dobbiamo suddividerla in tre differenti aree:
- Stack: è l’area di memoria dove vengono posizionate le variabili locali, i riferimenti, i valori di ritorno e le chiamate ai vari metodi. Ogni thread ha un suo Stack, quindi avendo N thread nella nostra applicazione saranno N differenti aree di memoria Stack
- Heap: all’interno del Heap troviamo tutte le variabili d’istanza e gli oggetti in generale. In base alla differente implementazione è possibile che sia organizzata in diverse aree per gestire al meglio la Garbage Collection
- PermGen: l’area di Permanent Generation è dove vengono salvate le rappresentazioni interne delle classi utilizzate (oltre ad altre cose)
Vediamo ora in dettaglio quali sono gli errori che possono verificarsi nel nostro programma Java
java.lang.StackOverflowError
Questo errore viene lanciato quando un Thread occupa tutto lo Stack a sua disposizione. Questo si verifica quando vengono allocate troppe variabili locali o quando le chiamate a funzione diventano troppe. Un esempio che permette di generare questo errore è una chiamata ricorsiva infinita, dove appunto per ogni chiamata alla funzione viene allocato spazio sullo Stack.
package com.javastaff.memory; public class StackBoom { private int count=0; public static void main(String args[]) { System.out.println("StackBoom: " + new StackBoom()); } @Override public String toString() { count++; System.out.println("StackBoom"+count); return this.toString(); } }
La grandezza dello Stack per ogni Thread varia in base alla JVM, al sistema operativo e alle variabili d’ambiente. Un valore standard è di 512k e per le JVM a 64 bit è più grande in quanto i riferimenti sono di 8 byte e non di 4. Se la nostra applicazione avesse nello stesso istante 100 Thread, l’insieme dei diversi Stack allocati sarebbe uguale a 50 MB.
La maggiorparte delle volte che si verifica questo problema abbiamo a che fare con problemi di ricorsione o comunque con troppi metodi che vengono richiamati in successione nello stesso Thread. Le possibilità che abbiamo sono quelle di ristrutturare le chiamate che vengono fatte oppure di intervenire a livello di JVM, innalzando lo spazio allocato per il singolo Stack.
Utilizzando il parametro -Xss si può definire lo spazio allocato, quindi se volessimo definire 1 MB di spazio per ogni Thread dovremmo avviare la JVM con il parametro -Xss1024k (o l’equivalente -Xss1MB).
java.lang.OutOfMemoryError: Java heap space
Tutti gli oggetti creati all’interno della JVM finisco nel Heap. Grazie al meccanismo del Garbage Collector un programmatore Java non si deve occupare di allocare e deallocare direttamente la memoria per questi oggetti, però ovviamente un’allocazione eccessiva può causare problemi. Nel seguente programma d’esempio vengono create stringhe in un loop infinito fino a quando la JVM alza la manina e dice STOP
package com.javastaff.memory; import java.util.ArrayList; import java.util.List; public class HeapBoom { public static void main(String[] args) { List<String> list=new ArrayList<String>(); for (int i=0;;i++) { String a = new String("HeapBoom"+i); System.out.println(a); list.add(a); } } }
All’interno del Heap, proprio per agevolare il meccanismo del Garbage Collector, esistono diverse aree di memoria dove possono risiedere i nostri oggetti. Molto dipende dall’implementazione della JVM ma possiamo in linea di massima definire le seguenti aree
- Young Generation: area dove vengono allocati inizialmente gli oggetti (Eden) o dove rimangono dopo un primo passaggio del Garbage Collector (Survivor)
- Tenured Generation: qui troviamo oggetti “vecchi” (Old Generation) della nostra applicazione, che non sono stati coinvolti in nessuna selezione da parte del Garbage Collector
Quando ci troviamo di fronte ad un OutOfMemoryError causato dallo space Heap possiamo intervenire in molti modi per effettuare un tuning adeguato della JVM. Come in altri casi, è ovvio che dovremmo cercare di capire se ci siamo imbattuti in un memory leak della nostra applicazione (o di librerie) oppure se effettivamente la nostra applicazione per come è stata concepita/realizzata ha bisogno di un tuning per cambiare dei parametri.
Di seguito vengono elencati diversi parametri che possiamo utilizzare per cambiare il comportamento della nostra JVM
- -Xmx : massima dimensione per Heap (ex: -Xmx1024)
- -Xms : minima dimendione per Heap. In un ambiente a 32bit non possiamo aumentare troppo questo parametro, in quanto non rimarrebbe molto spazio per altro
- -Xmn : dimenzione dell’area di Young Generation
- -XX:NewRatio : percentuale tra l’area di Tenured e Young Generation
- -XX:NewSize : dimensione dell’area di Young Generation in fase di avvio della JVM
- -XX:MaxNewSize : massima dimensione raggiungibile dalla Young Generation
- -XX:SurvivorRatio : il rapporto che c’è tra lo spazio di Survivor e l’Eden all’interno della Young Generation. Visto che i dettagli di ogni JVM cambia per quanto riguarda quest’area di memoria per effettuare un tuning corretto dovremmo prima sapere come è la sua specifica implementazione
- -XX:MinHeapFreeRatio: rapporto che definisce la minima area di memoria libera sempre disponibile. Inutile quando viene settato -Xmx = -Xms
- -XX:MaxHeapFreeRatio: rapporto che definisce la massima area di memoria libera sempre disponibile.
java.lang.OutOfMemoryError: PermGen space
Come già detto la Permanent Generation è una speciale area di memoria (non Heap e nemmeno Stack) dove risiedono principalmente le informazioni interne della JVM relative alla classi caricate dai vari ClassLoader. A partire dalla versione 7 di Java da questa area sono state rimosse le informazioni che venivano generate dal metodo String.intern() e dalla versione 8 quest’area di memoria non esiste più, o meglio cambia nome e posizione diventando il Metaspace. Per dettagli su questa nuova area vi rimando ad un articolo più dettagliato.
Per quanti di noi invece devono ancora combattere con questo problema, i motivi che si nascondono dietro a questo errore possono derivare da un esagerato utilizzo di librerie che fanno “magie” a livello di classloader, deploy/undeploy che lasciano allocati in memoria delle informazioni, librerie JDBC etc. etc. Come negli altri casi la prima cosa da fare è cercare di capire il motivo che si nasconde dietro a questo memory leak, altrimenti utilizzare i seguenti parametri per cambiare le impostazioni
- -XX:PermSize: valore iniziale della PermGen
- -XX:MaxPermSize: valore massimo della PermGen
java.lang.OutOfMemoryError : unable to create new native Thread
L’impossibilità da parte del sistema operativo sottostante di poter creare un nuovo Thread che viene richiesto dalla JVM può generare questo errore. Il seguente esempio di codice può generare questo tipo di errore
package com.javastaff.memory; public class NativeThreadBoom { public static void main(String[] args) { while(true){ new Thread(new Runnable(){ public void run() { try { Thread.sleep(10000000); } catch(InterruptedException e) { } } }).start(); } } }
Quando ci troviamo di fronte a questo problema una parte del nostro codice, o una libreria che viene utilizzata, potrebbe avere perso il controllo di una serie di Thread che quindi vengono lanciati senza un’adeguata gestione. Se stiamo utilizzando una JVM a 32 bit dobbiamo inoltre ricordarci che la dimensione del processo (JVM) è limitato nell’intervallo 2 GB – 4 GB, quindi potrebbe essere necessario un aggiornamento alla versione 64 bit se disponibile.
Il problema potrebbe anche essere relativo a dei limiti imposti dal sistema operativo, perchè se il numero di file descriptor è minore del numero di Thread che vogliamo gestire ovviamente il sistema operativo non riesce a gestire il nostro processo. In questo caso possiamo intervenire sul sistema per aumentare opportunamente questo valore. Un’altra possibilità che possiamo contemplare per cercare di risolvere questo problema è un tuning della JVM, diminuendo Stack o Heap della nostra applicazione (-Xss o -Xmx/-Xms).
Tool
Esistono molti modi/tool per effettuare un’analisi di problemi legati alla memoria. Qui di seguito vengono riportati i più famosi tool che ci permettono di effettuare un monitoraggio della nostra applicazione Java
Riferimenti
http://javaeesupportpatterns.blogspot.de/2013/02/java-8-from-permgen-to-metaspace.html
http://www.cubrid.org/blog/dev-platform/understanding-java-garbage-collection/
http://howtodoinjava.com/2013/10/19/usage-of-class-sun-misc-unsafe/
http://java.dzone.com/articles/understanding-sunmiscunsafe
https://blog.codecentric.de/en/2012/08/useful-jvm-flags-part-5-young-generation-garbage-collection/
https://blog.codecentric.de/en/2010/01/java-outofmemoryerror-eine-tragodie-in-sieben-akten/
http://www.slideshare.net/RafaelWinterhalter/a-topology-of-memory-leaks
http://www.avricot.com/blog/?post/2010/05/03/Get-started-with-java-JVM-memory-(heap%2C-stack%2C-xss-xms-xmx-xmn…)
http://crunchify.com/jvm-tuning-heapsize-stacksize-garbage-collection-fundamental/
http://javahash.com/java-memory-model-structures/
http://www.infoq.com/articles/Java_Garbage_Collection_Distilled
http://www.cubrid.org/blog/dev-platform/understanding-java-garbage-collection/
http://javaeesupportpatterns.blogspot.de/2013/02/java-8-from-permgen-to-metaspace.html

Looking for a right “about me”…
Commenti recenti