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
Menu
  • 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
Cerca
Chiudi

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

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

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.

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

Oracle Java Stored Procedures

Scrivere stored procedures direttamente in Java

La JSP Standard Tag Library

IV parte: JSTL e XML

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

Nella stessa serie
Loading...

Digital revolution: trasformare le aziende in ecosistemi digitali

III parte: Dagli obiettivi strategici alle iniziative operative

Digital revolution: La teoria

Tema 2: Il framework OKR

Digital revolution: trasformare le aziende in ecosistemi digitali

II parte: Un workshop per definire la vision

Bitcoin, blockchain e criptovaluta

V parte: Come funziona Bitcoin. Transazioni e UTXO

Digital revolution: La teoria

Tema 1: Approccio Scrum-like alle trasformazioni organizzative

Digital revolution: trasformare le aziende in ecosistemi digitali

I parte: Primo incontro.

Bitcoin, blockchain e criptovaluta

IV parte: Come funziona Bitcoin. Chiavi, indirizzi e wallet

Che cosa significa fare una trasformazione agile?

II parte: Le buone pratiche

Bitcoin, blockchain e criptovaluta

III parte: Come funziona Bitcoin. Rete e crittografia

Che cosa significa fare una trasformazione agile?

I parte: Le indispensabili premesse

Oltre Google Maps?

Una nuova alleanza per le mappe digitali

“Babbo… Ma te che lavoro fai?”

Come spiegare a un bambino (e alla sua maestra) il mestiere dell'agile coach

Bitcoin, blockchain e criptovaluta

II parte: Perché è nato Bitcoin

Microservizi: non solo tecnologia

Quanto influiscono sul business?

LEGO® Serious Play®

Che cosa è il metodo LSP?

State of Agile 2022

Qualche considerazione sul 16° rapporto

Bitcoin, blockchain e criptovaluta

I parte: Breve storia del denaro

Stili di leadership per l’agilità

Oltre il comando e controllo

Effetto Forrester e dinamiche dei sistemi di produzione

La storiella di una birra per comprendere il Lean

Fare “il tagliando” a Scrum

A due anni dalla pubblicazione della nuova guida

Veicoli elettrici ed esperienza utente

II parte: Riflessioni su app e UX

OKR, cadenze in Kanban e Flight Levels

Uscire dalla trappola dell’agilità locale

Continuous Delivery per il proprio team di sviluppo

Principi e pratiche

Il contratto è agile!

Gli elementi del contratto e l'Agilità

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