Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Java e i memory leak

Un esempio concreto di memory leak per vedere come anche Java possa avere problemi di gestione della memoria
Un esempio concreto di memory leak per vedere come anche Java possa avere problemi di gestione della memoria
Link copiato negli appunti

Java possiede ottime caratteristiche per la creazione di applicazioni professionali. In particolare è possibile, usando l'ambiente J2EE, creare applicazioni scalabili e affidabili in contesti multiutente. Purtroppo, però, ci sono casi in cui le applicazioni possono contenere del codice che causa un progressivo deterioramento del sistema, fino a un blocco dell'applicazione, se questa è stand-alone, o dell'application server/servlet container che la contiene, se è un'applicazione Web.

L'articolo mostra quando si può verificare questo comportamento, illustrando, con esempi concreti, come anche Java possa avere problemi di gestione della memoria.

Garbage collector: come funziona

Uno dei vantaggi del linguaggio Java è quello di non dover provvedere alla deallocazione degli oggetti creati in quanto ad occuparsene è un componente chiamato "garbage collector", che entra in azione in particolari circostanze. Il compito di questo componente è quello di eliminare oggetti non più in uso e, in questo modo, rendere di nuovo disponibile la memoria occupata da tali oggetti.

Purtroppo ci sono delle circostanze in cui il garbage collector fallisce nel suo compito; tali fallimenti non sono dati da una sua errata implementazione, ma da un uso improprio degli oggetti costruiti dall'applicazione. È bene considerare attentamente queste situazioni sia per evitarle che per riuscire ad individuarle. In questi casi si parla di "memory leak" (termine che si potrebbe tradurre con "falla nella memoria", anche se è comune usare il termine inglese).

Quando un oggetto è in uso

La prima cosa da comprendere è capire quando un oggetto è considerato "in uso" e, come tale, non può essere deallocato dal garbage collector. Un oggetto viene creato in presenza della parola chiave "new"; si può memorizzare tale oggetto assegnandolo ad una variabile:

Listato 1. Creazione di un nuovo oggetto e assegnazione ad una variabile

public unaClasse{
  public String unMetodo(){
    //[...]
    rif = new Date();
  }
}

Il garbage collector lo considera "in uso" fin quando il life time (o visibilità) della variabile (che ne contiene il riferimento) è valido. Pertanto, nel caso della seguente classe:

Listato 2. Life time della variabile che coincide con quello della classe

public unaClasse{
  Date rif;
  public String unMetodo(){
    rif = new Date();
  }
}

Il life time del riferimento coincide con quello della classe; nel caso:

Listato 3. Life time della variabile che coincide con quello del metodo

  public unaClasse{
    public String unMetodo(){
      Date rif;
      rif = new Date();
    }
}

Il riferimento ha il life time dell'esecuzione del metodo. Una prima classe di problemi di memory leak si verifica in presenza di oggetti con life time diverso e quello con life time maggiore mantiene un riferimento ad un oggetto con life time minore; per esempio:

Listato 4. Possibile memory leak

public unaClasse{
  Vector v = new Vector()
  public String unMetodo(){
    Date rif;
    rif = new Date();
    v.add(rif);
  }
}

In questo caso l'oggetto creato ha lo stesso life time del vettore e non, come prima, della variabile. Questo tipo di problemi è più comune di quanto si pensi sia nelle applicazioni Web, sia nelle applicazioni grafiche che usano il pattern listener/observer.

Però esistono anche memory leak più subdoli e difficili da scovare...

Memorizzare login e farne il log

Si consideri la seguente classe che esegue il log di utenti (di cui si vuol conoscere, per esempio, username e password):

Listato 5. Una classe per i log

public class TestCompletoClasseErrata {
  private final Hashtable logAccessi = new Hashtable();
  private final Hashtable utenti = new Hashtable();
}

Essa potrebbe contenere due Hashtable; la prima (logAccessi) esegue il log vero e proprio degli utenti; la seconda è una scorciatoia per reperire la posizione, all'interno della prima Hashtable, degli utenti creati:

Listato 6. il metodo addLog della classe TestCompletoClasseErrata

public void addLog(String username, String password){
  ClasseErrata chiave = new ClasseErrata(username, password);
  logAccessi.put( chiave, new Date());
  utenti.put(username, chiave);
}

Dove ClasseErrata è la seguente:

Listato 7. Il codice completo di ClasseErrata

public class ClasseErrata {
  private String user;
  private String pwd;
  
  public ClasseErrata(String userC, String pwdC){
    user = userC;
    pwd = pwdC;
  }
  
  public String getPwd() {
    return pwd;
  }
  
  public String getUser() {
    return user;
  }
  
}

Nella classe che fa il logging è necessario prevedere un metodo che elimina una certa entry di log; lo può fare, per esempio, dopo averla resa persistente sul db o da qualche altra parte:

Listato 8. altro metodo di TestCompletoClasseErrata

public void storeAndDelete(String username){
  Object toStore = logAccessi.get( utenti.get( username ));
  // rende persistente toStore...
  // [...]
  logAccessi.remove( utenti.get( username ) );
  utenti.remove(username);
}

Infine si può prevedere un metodo di utilità che ci dice quali utenti sono presenti nel log:

Listato 9. altro metodo di TestCompletoClasseErrata

public Set qualiUtenti(){
  return utenti.keySet();
}

Test e...problemi!

Ecco un semplice metodo main che esegue il test della classe (ma la logica potrebbe benissimo essere implementata in qualche filtro o servlet di un'applicazione Web):

Listato 10. Il main di TestCompletoClasseErrata

public static final void main(String[] args){
  TestCompletoClasseErrata logger = new TestCompletoClasseErrata();
  for(int iteraElimina=0; iteraElimina<100; iteraElimina++){
    for(int itera=0; itera<100000; itera++)
      logger.addLog("Ivan", "Venuti");
    logger.storeAndDelete("Ivan");
    
    System.out.println(logger.qualiUtenti().size());
  }
}

L'output del programma è il numero di utenti presenti nel log e, come ci si aspetta, è sempre 0. Quello che sorprende è che dopo alcune iterazioni del ciclo più esterno, l'applicazione si sblocca con un bel (anzi: bruttissimo!) OutOfMemoryError.

Figura 1. Il risultato dei memory leak: OutOfMemoryError
Il risultato dei memory leak: OutOfMemoryError

Essendo un main lanciato da console e con un ciclo piuttosto elevato, ce ne accorgiamo piuttosto presto. Lo stesso potrebbe capitare in momenti "random" in un'applicazione Web e scoprire quando non funziona potrebbe essere problematico!

Un po' di debug

Innanzitutto meglio togliere qualsiasi dubbio sull'algoritmo, effettivamente piuttosto bizzarro, della gestione dei log. Il problema è nella classe ClasseErrata (non per nulla si chiama così!).

Per scoprirlo dobbiamo però comprendere l'errore di fondo. Si prenda in considerazione il seguente esempio:

Listato 11. Test di uguaglianza con l'operatore ==

Object o1 = new Integer(1);
Object o2 = new Integer(1);
System.out.println(o1==o2);

Cosa vi aspettereste come output? Il risultato è "false" (provare per credere). Il motivo è presto detto: l'operatore == verifica se le variabili si riferiscono allo stesso oggetto. E questo non è vero in quanto sono stati creati due oggetti distinti.

Che dire del seguente codice?

Listato 12. Test di uguaglianza su oggetti reperiti da una Hashtable

Hashtable h = new Hashtable();
h.put(o1, o1);
h.put(o2, o2);
System.out.println(h.get(o1)==h.get(o2));

Il risultato è true, infatti il get dei due oggetti restituisce lo stesso oggetto, non due distinti; in pratica il secondo put "sovrascrive" il primo; per sincerarcene basta verificare che la Hashtable contiene solo un elemento; infatti la seguente istruzione stampa 1:

System.out.println(h.keySet().size());

Metodo equals

A questo punto può sorgere il dubbio che il test di uguaglianza non vada fatto usando l'operatore == ma usando il metodo equals. Infatti è proprio quello che accade nell'implementazione delle classi del package java.util e, in particolare, in Hashtable. E il metodo put verifica se esiste un oggetto uguale già presente, ma uguale nel senso del metodo "equals"! e siccome:

o1.equals(o2)

vale true per l'esempio, il secondo put sovrascrive il primo. Verifichiamo ClasseErrata eseguendo:

Listato 15. Un metodo che evidenzia come equals non fa quel che dovrebbe per ClasseErrata

public static final void main(String[] args){
  Object o1 = new ClasseErrata("PIPPO", "PLUTO");
  Object o2 = new ClasseErrata("PIPPO", "PLUTO");
  System.out.println(o1.equals(o2));
}

In questo caso la stampa dà valore false quando ci si aspetterebbe il valore true. Ritornando all'esempio che esauriva la memoria, si scopre che l'Hashtable logAccessi aveva un numero esorbitante di elementi. Il motivo era dato dal metodo addLog:

Listato 16. Il punto dove vengono generati i memory leak

public void addLog(String username, String password){
  ClasseErrata chiave = new ClasseErrata(username, password);
  logAccessi.put( chiave, new Date());
  utenti.put(username, chiave);
}

Ogni nuovo "put" di un oggetto di tipo ClasseErrata corrispondeva alla creazione di un nuovo elemento nella Hashtable, anziché (come ci si aspetterebbe) sovrascrivere l'entry precedente quando username/password sono gli stessi. Lo si capisce meglio eseguendo:

Listato 17. Un semplice test che evidenzia il memory leak

logger.addLog("Ivan", "Venuti");
logger.addLog("Ivan", "Venuti");
System.out.println(logger.utenti.size());
System.out.println(logger.logAccessi.size());

La prima stampa dà valore 1, la seconda 2. Ed eseguendo, alla fine:

logger.storeAndDelete("Ivan");

elimina l'unico elemento di utenti ma solo uno di logAccessi. L'altro elemento di logAccessi resterà come memory leak e, iterando diverse volte il codice, satura le risorse e blocca il programma.

La correzione

Questo campanello d'allarme ci potrebbe costringere a rivedere le specifiche del linguaggio e scoprire che dovremmo implementare correttamente almeno due metodi: equals e hashCode. Ecco una possibile loro implementazione:

Listato 19. La classe come dovrebbe essere: implementa i metodi hashCode ed equals

public class ClasseCorretta {
  private String user;
  private String pwd;
  
  public int hashCode(){
    return (""+user+pwd).hashCode();
  }
  public boolean equals(Object o){
    if (o==null || !o.getClass().equals(this.getClass()))
      return false;
    ClasseCorretta oCast = (ClasseCorretta) o;
    boolean m1 = (user==null && oCast.getUser()==null || user!=null && user.equals(oCast.getUser()));
    boolean m2 = (pwd==null && oCast.getPwd()==null || pwd!=null && pwd.equals(oCast.getPwd()));
    return m1 && m2;
  }
  
  public ClasseCorretta(String userC, String pwdC){
    user = userC;
    pwd = pwdC;
  }
  
  public String getPwd() {
    return pwd;
  }
  
  public String getUser() {
    return user;
  }
  
}

Questa semplice modifica porta a far funzionare l'algoritmo di logging senza altre modifiche.

Conclusione

Si è visto come anche Java possa soffrire di memory leak. L'errore di implementazione di una classe non era così banale da risultare subito evidente (teoricamente il logging avveniva correttamente) e il side effect del memory leak si rivelava solo dopo un certo numero di iterazioni.

Tali errori non sono affatto così rari. Molti dei bug del JDK stesso sono dovuti a memory leak nell'implementazione di alcune classi.

Tra i problemi dei memory leak è che si scoprono solo facendo dei test di carico sulle applicazioni (com'è stato fatto con il metodo main che portava al blocco del programma) oppure verificando gli oggetti allocati in memoria e non rilasciati in seguito all'esecuzione del garbage collector. Quest'ultima analisi necessita di strumenti appositi.

Sitografia


Ti consigliamo anche