Mokabyte

Dal 1996, architetture, metodologie, sviluppo software

  • Argomenti
    • Programmazione & Linguaggi
      • Java
      • DataBase & elaborazione dei dati
      • Frameworks & Tools
      • Processi di sviluppo
    • Architetture dei sistemi
      • Sicurezza informatica
      • DevOps
    • Project Management
      • Organizzazione aziendale
      • HR
      • Soft skills
    • Lean/Agile
      • Scrum
      • Teoria della complessità
      • Apprendimento & Serious Gaming
    • Internet & Digital
      • Cultura & Società
      • Conferenze & Reportage
      • Marketing & eCommerce
    • Hardware & Tecnologia
      • Intelligenza artificiale
      • UX design & Grafica
  • Ultimo numero
  • Archivio
    • Archivio dal 2006 ad oggi
    • Il primo sito web – 1996-2005
  • Chi siamo
  • Ventennale
  • Libri
  • Contatti
  • Argomenti
    • Programmazione & Linguaggi
      • Java
      • DataBase & elaborazione dei dati
      • Frameworks & Tools
      • Processi di sviluppo
    • Architetture dei sistemi
      • Sicurezza informatica
      • DevOps
    • Project Management
      • Organizzazione aziendale
      • HR
      • Soft skills
    • Lean/Agile
      • Scrum
      • Teoria della complessità
      • Apprendimento & Serious Gaming
    • Internet & Digital
      • Cultura & Società
      • Conferenze & Reportage
      • Marketing & eCommerce
    • Hardware & Tecnologia
      • Intelligenza artificiale
      • UX design & Grafica
  • Ultimo numero
  • Archivio
    • Archivio dal 2006 ad oggi
    • Il primo sito web – 1996-2005
  • Chi siamo
  • Ventennale
  • Libri
  • Contatti

Nel numero:

97 giugno
, anno 2005

Individuare i Memory Leaks nelle applicazioni Java

Ottimizzare la gestione della memoria

Avatar
Doriano Gallozzi

Doriano Gallozzi è nato a Roma nel 1964 e si è laureato in Ingegneria Elettronica (indirizzo Informatica) nel 1989. Da sempre interessato a problematiche di analisi, progettazione e sviluppo di Sistemi Informativi, ha lavorato per diversi anni in aziende del settore informatico italiano (gruppo ENI, gruppo Telecom Italia), dove ha acquisito diverse esperienze tanto nel campo della progettazione e sviluppo del software (in ambiente M/F come in ambiente distribuito) quanto nel campo dei RDBMS (DBA su diversi progetti per clienti finali quali Telecom Italia). Da gennaio 2000 lavora nella Divisione Prevendita di Compuware Italia. La sua attività verte principalmente sulla piattaforma J2EE e tecnologie correlate, ma anche su ambiti tecnologici quali le soluzioni di Project Management per IT-Governance, i Portali Web, gli strumenti di Business Process Automation, l‘Enterprise Application Integration.

MokaByte

Individuare i Memory Leaks nelle applicazioni Java

Ottimizzare la gestione della memoria

Picture of Doriano Gallozzi

Doriano Gallozzi

  • Questo articolo parla di: Java, Programmazione & Linguaggi

Chi scrive applicazioni in Java non deve preoccuparsi di gestire la memoria: ma è davvero così? E allora
perchè le nostre applicazioni Java sono poco scalabili e allocano tanta memoria? Forse perchè non la gestiscono in modo efficiente…

Introduzione

Qualunque sviluppatore Java, indipendentemente dalla propria capacità  o esperienza, si è con ogni probabilità  trovato faccia a faccia con comportamenti inattesi da parte delle applicazioni su cui lavorava. Ad esempio, applicazioni web che funzionano regolarmente con 1800 utenti contemporaneamente connessi e che con “solo” 100 utenti in più rallentano in modo inaccettabile. Ogni volta che una applicazione si comporta in modo inaspettatamente negativo, ciò può dipendere da diversi fattori, non soltanto dovuti ad algoritmi inefficienti ma anche a gestione scorretta della memoria. Qualcuno potrebbe storcere la bocca, giacchè è noto che in Java esiste un meccanismo di Garbage Collection che “ripulisce” automaticamente tutto ciò che non viene più utilizzato…..ma è proprio vero che le cose stanno così? Se il “Garbage Collector” rimuove tutto ciò che vede, cosa accade se esistono oggetti che non riesce a vedere? Che non li rimuove!!! Tali oggetti pertanto creano delle “falle” nella memoria, ossia quel tipo di problemi noti come “Memory Leaks” (rif [1])…..”ma no”, avrebbe detto una nota pubblicità  di un detersivo diversi anni fa, “non esiste sporco impossibile!” Impossibile no, ma difficile da individuare certamente sì. E‘ lecito chiedersi se esistono delle tecniche per prevenire queste situazioni, quali sono, quando e come utilizzarle. In questo articolo cercheremo di dare alcune risposte.

Problemi nelle applicazioni Java

Quando una applicazione Java presenta problemi o malfunzionamenti, è generalmente possibile individuare una serie di “sintomi” che possono essere raggruppati in una o più delle seguenti categorie:

  • Scalabilità , ad esempio codice che funziona correttamente almeno fino a quando l‘insieme degli utenti simultanei non raggiunge un valore critico, dopo il quale il comportamento degrada
  • Performance percepita, ad esempio ritardi durante lo start-up della applicazione, o durante l‘esecuzione di una semplice operazione (ad es. disegnare una finestra)
  • Eccessivo utilizzo della CPU, ad esempio rallentamenti nell‘eseguire operazioni grafiche o transazioni su DBMS
  • Condivisione di risorse critiche, ad esempio quando in una applicazione multi-thread diversi thread concorrono nell‘utilizzo della memoria

Il sorgere di questi inconvenienti non è necessariamente legato alla fase di sviluppo o a quella del testing o all‘esercizio, nel senso che essi possono presentarsi in qualsiasi momento….e purtroppo il costo del loro presentarsi non è lo sempre lo stesso: Giga Group ha stabilito un costo superiore del 50% nel caso in cui i problemi insorgano durante la fase di produzione: in sostanza, più tardi si rilevano i problemi, maggiore è il costo da sostenere per eliminarli.

Il concetto di Profiling

Quanto detto evidenzia la necessità  di intervenire prima possibile, magari già  durante le fasi dello sviluppo di una applicazione, al fine di permettere agli sviluppatori di “correggere il tiro” e di prevenire il maggior numero di problemi al più presto possibile. In definitiva, occorre “profilare” in modo opportuno la nostra applicazione. Ma cosa significa esattamente profilare (traduzione italiana del verbo inglese “profiling”)? Come descritto in modo molto efficace in rif.[2], si intende per “profiling”:

  • la capacità  di monitorare e tracciare gli eventi che accadono a run time
  • la capacità  di tracciare il costo di tali eventi
  • la capacità  di attribuire tale costo a specifiche parti della applicazione

Ad esempio, un profiler può fornire informazioni su quale parte della nostra applicazione consuma la maggiore quantità  di tempo di CPU, o su quale parte della nostra applicazione alloca la maggiore quantità  di memoria.
A questo punto nascono due domande: quando effettuare il profiling e come procedere ad effettuarlo.
Un testo fondamentale sulle problematiche di performance in ambiente Java (rif.[3]) risponde alla prima domanda, in quanto mostra proprio come i processi di testing possano essere efficacemente integrati e completati da quelli di profiling, immergendo poi il tutto nel ciclo di vita della applicazione. Secondo quanto affermato dagli autori, una volta che la nostra applicazione risulta funzionalmente corretta, è il momento di verificare che la performance sia accettabile, ed eventualmente impostare una adeguata infrastruttura di profiling. La figura 1 riassume proprio questo tipo di approccio, in cui si nota in particolare anche che i risultati ricavati durante il profiling possono innescare successive fasi di analisi, progettazione, codifica.

Adesso occorre rispondere alla seconda domanda, e cioè come effettuare il profiling, e cioè con quali metodologie, strategie, strumenti. Le aree tipiche in cui si attuano le procedure di profiling riguardano il tempo di esecuzione (la Performance Analysis), la copertura del codice eseguito (Code Coverage), ma soprattutto – area decisamente più critica – l‘Analisi della Memoria (Memory Analysis), proprio per individuare i già  citati “Memory Leaks”. E‘ su quest‘ultima area che soffermeremo la nostra attenzione.
Per tutti gli argomenti trattati saranno presentati diversi esempi pratici, che verranno analizzati e trattati utilizzando la soluzione Compuware DevPartner Java (rif.[4]).

L‘ occupazione della RAM

Quale “impronta” lascia nella memoria runtime (quella comunemente indicata come “heap”) la nostra applicazione? Tale informazione va sotto il nome di RAM Footprint. Scegliamo quindi nella nostra applicazione una parte che consideriamo “sospetta” perchè pensiamo possa consumare tanta RAM: essa costituirà  la cosiddetta parte “profilata” della applicazione. Perchè è importante conoscere il “RAM Footprint” della parte profilata della nostra applicazione? Perchè così siamo in grado, in modo abbastanza rapido, di verificare se la nostra applicazione è o no il principale “RAM allocator” durante l‘esecuzione, quello che in definitiva si aggiudica una fetta un pò troppo grossa di RAM.
Nella figura 2 si può notare un “pie chart” (diagramma a torta) che illustra le varie “fette” di RAM allocate da una applicazione durante la sua esecuzione. I diversi colori evidenziano le parti profilate e non profilate della applicazione. Nel caso in esame si nota come la maggior parte della RAM sia stata occupata dalla parte profilata della nostra applicazione, che quindi necessita di una attenta analisi al fine di comprendere i motivi che hanno portato a questo fatto.

I Memory Leaks

Obiettivo di una buona analisi della memoria è quello di individuarne le “falle”, ovvero quei difetti della nostra applicazione – detti “Memory Leaks” – che portano ad un uso improprio o inefficiente della memoria stessa e che possono quindi causare problemi quali rallentamenti generali della performance e anche scarsa scalabilità .
Come individuare i Memory Leaks? Quali sono le cause dei Memory Leaks? L‘esperienza dimostra che possono essere molteplici. Facciamo un semplice esempio. Consideriamo il codice che gestisce una struttura dati di tipo stack (la classica “pila”). Come noto, le operazioni fondamentali che possono essere effettuate sono quella di “inserisci elemento nello stack” (push) e “estrai elemento dallo stack” (pop). Immaginiamo di analizzare una semplice applicazione Java che gestisce uno stack e cerchiamo i potenziali Memory Leaks. Eseguendo una sessione di analisi otteniamo il diagramma riportato in figura 3

Si può intuitivamente già  vedere che qualcosa nella nostra applicazione non va. In effetti, il diagramma che rappresenta la occupazione della memoria dovrebbe tornare dopo un certo tempo a zero in virtù delle Garbage Collection responsabili della eliminazione degli oggetti allocati. Il fatto che il diagramma si mantenga alto e non torni ai livelli iniziali desta dei sospetti. Se al termine della nostra sessione di analisi, sono rimasti in memoria degli oggetti, essi vengono riguardati come potenziali Memory leaks e ciò fornisce utili indicazioni su dove spingere ulteriori indagini. Ogni volta che il Garbage Collector non individua ed elimina un oggetto, significa che esiste almeno una reference ad esso, e probabilmente non dovrebbe esserci….. Il nostro strumento di profiling ci fornisce poi anche una analisi dettagliata delle varie parti della applicazione responsabili di aver creato potenziali “Leaks” (figura 4); vediamo di sfruttarle nel modo opportuno.

Se concentriamo la nostra attenzione sulla prima delle classi caratterizzate dal maggior numero medio di “leaked bytes”, la classe Object[], possiamo visualizzarne anche il relativo Object Reference Path, che ci fornisce informazioni sul perchè quel determinato oggetto è in memoria, in quanto referenziato da altri. In figura 5 vediamo appunto tale grafo.

Facendo ora click sul nodo in giallo (Object[], oggetto della nostra analisi), ci viene presentato il frammento di codice in cui la struttura di dati Object[] viene allocata nel metodo pop() (figura 6)

Non è difficile accorgersi che basterebbe inserire una semplice istruzione di “ripulitura” tipo elements[size] = null per eliminare il Memory Leak. In sostanza, in questo caso semplice, abbiamo a che fare con un metodo pop()che quando estrae gli elementi non li “ripulisce”, quindi essi continuano a occupare memoria e quindi non vengono eliminati dal Garbage Collector ….la nuova versione “pulita” del nostro frammento di codice potrebbe presentarsi così:

package stackLeakExample;public class Stack{private Object[] elements; // Buffer per contenere lo stackprivate int size = 0;public Stack(int initialCapacity) {this.elements = new Object[initialCapacity];}public void push(Object e) {ensureCapacity();elements[size++] = e;}public Object pop() {Object e = elements[--size];// STATEMENT INSERITO PER ELIMINARE IL LEAK!!!elements[size] = null;return e;}private void ensureCapacity() {if (elements.length == size) {Object[] oldElements = elements;elements = new Object[2*elements.length+1];System.arraycopy(oldElements, 0, elements, 0, size);}}}

Abbiamo esaminato in questo semplice esempio una delle possibili cause dei Memory Leaks, dovuta a una errata inizializzazione e re-inizializzazione degli elementi estratti dallo stack. Altre possibili cause possono aversi ad esempio quando viene aperta una connessione verso un database che non viene poi successivamente chiusa. Esistono poi alcune categorie di oggetti che, per la loro stessa natura, possono portare a intasare gravemente la memoria favorendo la comparsa di problemi di scalabilità : essi sono i cosiddetti “oggetti temporanei”, quegli oggetti cioè che vengono mantenuti in memoria anche quando non servono più alla applicazione che li ha utilizzati. Vediamoli un pò più da vicino.

Gli oggetti temporanei

Come già  detto più sopra, uno dei punti di forza universalmente riconosciuti di Java è la gestione automatica della memoria, grazie alla presenza del Java Garbage Collector. Come specificato in rif.[5], è però necessario conoscerne bene i meccanismi di di funzionamento, altrimenti si rischia di provocare l‘insorgere di oggetti che in memoria vengono mantenuti per tempi inaccettabilmente lunghi, riducendo la performance della nostra applicazione e consumando anche elevate porzioni di RAM (RAM footprint, v. più sopra). Consideriamo il frammento di codice seguente:

java.lang.String:String r = new String ();For (int I-0< limit; I+=1) {r = r + compute(i);}return r;

Per ciascuna iterazione, una nuova occorrenza di String r viene allocata come oggetto temporaneo e la r corrente viene copiata nella nuova istanza dopo che compute(i)è stato aggiunto alla nuova stringa r. Tutto questo accade per ogni iterazione. Una tecnica comunemente utilizzata e nota per poter risolvere questo problema consiste nell‘utilizzare StringBuffer al posto di String, proprio allo scopo di ridurre il numero di oggetti temporanei allocati e produrre codice più veloce e scalabile. Il frammento appena visto diventa quindi

java.lang.StringBuffer:StringBuffer r = new StringBuffer();For (int i=0; i< limit; i+=1) {r.append(compute(i));}return r.toString();

Nella vita reale le cose non sono sempre così semplici e dirette. Effettuare una analisi approfondita del codice (che può essere tanto e neppure scritto da noi.....) e comprendere quali parti sono responsabili della creazione di pericolosi oggetti temporanei può essere un compito piuttosto complesso da svolgere, spesso complesso in modo eccessivo. Ecco dunque l‘utilità  di poter disporre di appositi tool, che permettano di effettuare analisi della memoria con ricerca degli oggetti temporanei e mettano a disposizione anche il relativo Object Graph, fondamentale per "tracciare" con precisione chi ha invocato chi, la memoria utilizzata dai vari oggetti e anche la storia degli oggetti temporanei, quanti ne sono stati creati e che tipo di "persistenza" possiedono, distinguendo gli "short lived", eliminati alla prima garbage collection, i "medium lived", eliminati dopo la seconda o terza, i "long lived", eliminabili dopo varie passate se non addirittura indistruttibili. Se si ha a che fare con applicazioni che provocano l‘insorgere di numerosi oggetti temporanei e si esegue su di essi una Memory Analysis di tipo Object Lifetime, si ottiene un andamento a "picchi" come quello riportato in figura 7.

In seguito a ciò, e in modo analogo a quanto visto a proposito dei Memory Leaks, è possibile ottenere ulteriori informazioni sulle diverse parti della applicazione che hanno creato oggetti temporanei e spingere oltre le proprie indagini, fino a scoprire i "colpevoli"...

Conclusioni

Riuscire a scrivere codice Java per applicazioni business-critical che sia, oltre che funzionalmente corretto, anche performante e scalabile, è compito estremamente complesso e gravoso. Proprio per questi motivi una accorta ed efficienze conoscenza dei meccanismi di gestione della memoria in Java è di fondamentale importanza, così come l‘utilizzo combinato dei diversi meccanismi di analisi applicabili. Non si tratta, come abbiamo visto, di effettuare soltanto una analisi generalizzata volta ad individuare la porzione di RAM occupata, ma di spingere l‘analisi della memoria a 360 gradi, con l‘obiettivo di individuare i tanto temuti Memory Leaks, causati da una molteplicità  di fattori tra cui - molto importanti - i cosiddetti oggetti temporanei; chi lavora con Java sa bene che non è possibile evitarne del tutto l‘impiego, ma è certo che una accorta e consapevole gestione degli stessi porta senz‘altro ad una maggiore qualità  ed efficienza del codice.

RIferimenti

[1]
G. Begic, "Monitoring Object Creation in Java Application"
http://www-106.ibm.com/developerworks/rational/library/content/RationalEdge/jun01/MonitoringObjectCreationJune01.pdf

[2]
D. Viswanathan - S. Liang, "Java Virtual Machine Profiler Interface"
http://www.research.ibm.com/journal/sj/391/viswanathan.html

[3]
S. Wilson - J. Kesselman, "Java Platform Performance - Strategies and Tactics"
http://java.sun.com/docs/books/performance/1st_edition/html/JPTitle.fm.html

[4]
DevPartner Java Home Page
http://www.compuware.com/products/devpartner/java.htm

[5]
Compuware White Paper, "Temporary Objects - Managing the Java Garbage Collector for application quality and performance"
http://www.compuware.com/dl/garbage.pdf

Avatar
Doriano Gallozzi

Doriano Gallozzi è nato a Roma nel 1964 e si è laureato in Ingegneria Elettronica (indirizzo Informatica) nel 1989. Da sempre interessato a problematiche di analisi, progettazione e sviluppo di Sistemi Informativi, ha lavorato per diversi anni in aziende del settore informatico italiano (gruppo ENI, gruppo Telecom Italia), dove ha acquisito diverse esperienze tanto nel campo della progettazione e sviluppo del software (in ambiente M/F come in ambiente distribuito) quanto nel campo dei RDBMS (DBA su diversi progetti per clienti finali quali Telecom Italia). Da gennaio 2000 lavora nella Divisione Prevendita di Compuware Italia. La sua attività verte principalmente sulla piattaforma J2EE e tecnologie correlate, ma anche su ambiti tecnologici quali le soluzioni di Project Management per IT-Governance, i Portali Web, gli strumenti di Business Process Automation, l‘Enterprise Application Integration.

Facebook
Twitter
LinkedIn
Avatar
Doriano Gallozzi

Doriano Gallozzi è nato a Roma nel 1964 e si è laureato in Ingegneria Elettronica (indirizzo Informatica) nel 1989. Da sempre interessato a problematiche di analisi, progettazione e sviluppo di Sistemi Informativi, ha lavorato per diversi anni in aziende del settore informatico italiano (gruppo ENI, gruppo Telecom Italia), dove ha acquisito diverse esperienze tanto nel campo della progettazione e sviluppo del software (in ambiente M/F come in ambiente distribuito) quanto nel campo dei RDBMS (DBA su diversi progetti per clienti finali quali Telecom Italia). Da gennaio 2000 lavora nella Divisione Prevendita di Compuware Italia. La sua attività verte principalmente sulla piattaforma J2EE e tecnologie correlate, ma anche su ambiti tecnologici quali le soluzioni di Project Management per IT-Governance, i Portali Web, gli strumenti di Business Process Automation, l‘Enterprise Application Integration.

Picture of Doriano Gallozzi

Doriano Gallozzi

Doriano Gallozzi è nato a Roma nel 1964 e si è laureato in Ingegneria Elettronica (indirizzo Informatica) nel 1989. Da sempre interessato a problematiche di analisi, progettazione e sviluppo di Sistemi Informativi, ha lavorato per diversi anni in aziende del settore informatico italiano (gruppo ENI, gruppo Telecom Italia), dove ha acquisito diverse esperienze tanto nel campo della progettazione e sviluppo del software (in ambiente M/F come in ambiente distribuito) quanto nel campo dei RDBMS (DBA su diversi progetti per clienti finali quali Telecom Italia). Da gennaio 2000 lavora nella Divisione Prevendita di Compuware Italia. La sua attività verte principalmente sulla piattaforma J2EE e tecnologie correlate, ma anche su ambiti tecnologici quali le soluzioni di Project Management per IT-Governance, i Portali Web, gli strumenti di Business Process Automation, l‘Enterprise Application Integration.
Tutti gli articoli
Nello stesso numero
Loading...

La prorgammazione concorrente

IV parte: l‘uso dei lock

Web Services

I parte: il punto sulla standardizzazione

Multimedialità su J2ME

II parte: la gestione dell‘audio

Gli ascoltatori d‘evento

I molti modi per intercettare un evento swing

Identity Management

Gestire l‘identità per massimizzare i processi di business

MokaCMS – Open Source per il Web Content Management

Vparte: da XML ad HTML utilizzando XSLT

Oracle Java Stored Procedures

Scrivere stored procedures direttamente in Java

La JSP Standard Tag Library

IV parte: JSTL e XML

Nella stessa serie
Loading...

Vibe Coding: sviluppare il prodotto con l’AI

I parte: I fondamenti del processo

Talento, performance, carriera: uno sguardo d’insieme

II parte: Come viene favorito il talento in azienda?

Modelli LLM: Come funzionano?

I parte: ANN (Artificial Neural Network), reti neurali artificiali

LLMs models: how do they work?

Part 1: Artificial Neural Networks

Adattare l’agilità ai contesti: una chiave di lettura

II parte: Strumenti per decisioni complesse

Il dilemma del prigioniero

Un “gioco serio” per comprendere la cooperazione

Accessibilità in team di prodotto: sfide, normative e best practice

II parte: Analisi di un caso reale

Adattare l’agilità ai contesti: una chiave di lettura

I parte: Un caso di studio con le sue peculiarità

Talento, performance, carriera: uno sguardo d’insieme

I parte: Che cosa è il talento?

Accessibilità in team di prodotto: sfide, normative e best practice

I parte: Cosa è l’accessibilità e perché implementarla

Il web al tempo della GEO (Generative Engine Optimization)

II parte: Strategie per strutturare i contenuti

Un backlog non tanto buono

II parte: Caratteristiche e ruolo del backlog.

FIWARE: Open APIs for Open Minds

V parte: Implementazione del sistema di ricarica

Il web al tempo della GEO (Generative Engine Optimization)

I parte: Struttura e ricerca delle informazioni

Un backlog non tanto buono

I parte: Un progetto con qualche difficoltà

DDD, microservizi e architetture evolutive: uno sguardo d’insieme

X parte: Il ruolo del Software Architect

FIWARE: Open APIs for Open Minds

IV parte: Sistema di ricarica intelligente per veicoli elettrici

Tra Play14 e serious gaming

Un ponte tra gioco e apprendimento

DDD, microservizi e architetture evolutive: uno sguardo d’insieme

IX parte: Event Sourcing is not Event Streaming

FIWARE: Open APIs for Open Minds

III parte: Tecnologie e implementazione

Agilità organizzativa

II parte: Qualche caso d’esempio

Agilità organizzativa

I parte: Individui e interazioni nelle aziende moderne

FIWARE: Open APIs for Open Minds

II parte: Generic Enablers per costruire ecosistemi smart

Intelligenza artificiale e industria

Riflessioni sull’uomo e sulla macchina

Mokabyte

MokaByte è una rivista online nata nel 1996, dedicata alla comunità degli sviluppatori java.
La rivista tratta di vari argomenti, tra cui architetture enterprise e integrazione, metodologie di sviluppo lean/agile e aspetti sociali e culturali del web.

Imola Informatica

MokaByte è un marchio registrato da:
Imola Informatica S.P.A.
Via Selice 66/a 40026 Imola (BO)
C.F. e Iscriz. Registro imprese BO 03351570373
P.I. 00614381200
Cap. Soc. euro 100.000,00 i.v.

Privacy | Cookie Policy

Contatti

Contattaci tramite la nostra pagina contatti, oppure scrivendo a redazione@mokabyte.it

Seguici sui social

Facebook Linkedin Rss
Imola Informatica
Mokabyte