Estrarre informazioni utili dai tuoi file di log è fondamentale per il successo della tua applicazione Java. I dati di log ti daranno un’idea preziosa delle prestazioni, della stabilità e dell’usabilità della tua applicazione.

Analizzare i dati di log può sembrare noioso, ma non deve esserlo. Ci sono una varietà di strumenti per leggere, analizzare e consolidare i dati di log. Strumenti di base a riga di comando come grep, uniq e sort possono combinare ed estrarre informazioni utili dai file di log. Analizzatori di log più avanzati come Logstash o Fluentd possono estrarre i dati chiave dai vostri log in token facilmente ricercabili. I servizi di log basati sul cloud come SolarWinds® Loggly® memorizzano i dati di log per voi e offrono sofisticate capacità di analisi, eliminando la necessità di mantenere i log da soli.

Questa sezione esplora alcuni metodi e strumenti per analizzare i log con l’obiettivo di migliorare le vostre applicazioni.

Trovare le eccezioni più comuni

Trovare le eccezioni più comuni può aiutarvi a individuare le aree di scarse prestazioni nella vostra applicazione Java. La maggior parte dei framework di log registra il tipo di eccezione, il messaggio di eccezione e il metodo in cui si è verificata l’eccezione. Usando Log4j, un log delle eccezioni sarà simile a uno dei seguenti.

09:54:44.565 ERROR Log4jTest.MathClass - java.lang.ArithmeticException: / by zero10:00:10.157 ERROR Log4jTest.Log4jTest - java.io.FileNotFoundException: myFile (No such file or directory)10:00:10.157 ERROR Log4jTest.Log4jTest - java.io.FileNotFoundException: newFile (No such file or directory)

Se il vostro output include tracce di stack, vedete la sezione Parsing Multiline Stack Traces di questa guida.

Iniziamo con un semplice esempio usando il popolare strumento GNU grep. Poi, mostreremo come uno strumento di gestione dei log può rendere tutto ancora più facile.

Trovare le eccezioni per tipo usando Grep

Il seguente comando Unix troverà le eccezioni, estrarrà il tipo di eccezione e conterà il numero di occorrenze. grep è un popolare strumento a riga di comando che esegue il pattern matching, mentre uniq e sort raggruppano e ordinano i risultati, rispettivamente:

$ grep -o "\w*Exception" myLog.log | sort -r | uniq -c

In questo esempio, l’espressione regolare \w*Exception corrisponde a qualsiasi parola che finisce con “Exception.” Il flag -o dice a grep di stampare solo le parti dell’output che corrispondono alla stringa di ricerca. sort -r ordina il risultato in ordine inverso, mentre uniq -c raggruppa i risultati e conta il numero di occorrenze. Come risultato, ci ritroviamo con un conteggio delle eccezioni per tipo.

2 FileNotFoundException1 ArithmeticException

Possiamo anche usare grep per cercare nel log ogni specifica istanza di queste eccezioni.

$ grep ArithmeticException myLog.log09:54:44.565 ERROR Log4jTest.Log4jTest - java.lang.ArithmeticException: / by zero

Questo restituisce ogni linea contenente un’istanza della stringa di ricerca ArithmeticException, con la stringa di ricerca stessa evidenziata nell’output.

Finding Exceptions by Class Using Grep

Puoi anche cercare le eccezioni in base alla classe in cui sono avvenute. Per eventi di log a linea singola, il seguente comando raggruppa il numero di eccezioni per classe e per tipo. grep estrae il nome della classe cercando una parola seguita da un particolare carattere (questo esempio usa un trattino, anche se qualsiasi carattere può essere usato purché sia unico per la voce). Anche se questo carattere non è essenziale, ci aiuta a localizzare il nome della classe nell’evento di log. Con l’operatore OR (indicato con |), grep estrarrà anche il nome dell’eccezione cercando una parola che contenga la stringa “Exception.”

sed è un’altra utilità a riga di comando che può essere usata per formattare l’output di grep. In questo esempio, sed rimuove il carattere speciale dal nostro output così come qualsiasi carattere di nuova riga. L’output viene poi indirizzato ai comandi uniq e sort per raggruppare e ordinare rispettivamente i risultati.

$ grep -o "w* -|\w*Exception" myLog.log | sed 'N; s/ -n/ /' | sort -r | uniq -c

Il risultato ci porta più vicino alle aree chiave dei problemi:

2 Log4jTest FileNotFoundException1 MathClass ArithmeticException

Con questi risultati, possiamo usare grep per trovare maggiori dettagli sui problemi che si verificano in classi specifiche. Per esempio, il seguente comando recupera le eccezioni che si sono verificate nella classe MathClass.

$ grep -e "MathClass.*Exception" myLog.log09:54:44.565 ERROR Log4jtest.MathClass - java.lang.ArithmeticException: / by zero

Utilizzando una soluzione di gestione dei log

La maggior parte delle soluzioni di gestione dei log offre modi per raggruppare e cercare voci di log in base al tipo di log, al contenuto del messaggio, alla classe, al metodo e al thread. Se i vostri log vengono già analizzati e memorizzati, molte soluzioni possono graficare e ordinare le eccezioni in base al numero di occorrenze. Questa è un’operazione “punta e clicca” in Loggly, quindi non c’è bisogno di memorizzare complicati comandi grep. Loggly indicizza anche i record di log, rendendo le ricerche e i conteggi molto più veloci di grep. Per esempio, possiamo usare l’esploratore di campi per vedere un elenco di eccezioni e la loro frequenza, quindi fare clic su un’eccezione per visualizzare tutti i log pertinenti.

Utilizzando l’esploratore di campi Loggly per trovare rapidamente i log per tipo di eccezione.

Loggly offre anche strumenti di visualizzazione che possono essere più informativi dell’output testuale di grep. Grafici come questo possono aiutarvi a dare priorità alle risorse per gli sprint di sviluppo o per le patch che seguono una nuova release.

Cartografia delle eccezioni per frequenza in Loggly.

Debugare i problemi di produzione

Quando si tratta di problemi di produzione, il tempo è essenziale. Un errore critico nella tua applicazione non solo lascerà i tuoi utenti insoddisfatti, ma ridurrà anche le vendite e la fiducia nella tua applicazione o servizio.

Nella maggior parte dei casi, la risoluzione di un problema può essere suddivisa in tre passi chiave:

  1. Raccogliere informazioni sul problema
  2. Identificare la causa del problema
  3. Trovare una soluzione e prevenire il ripetersi del problema

Raccogliere informazioni sul problema

Il primo passo è raccogliere informazioni sul problema. Raccogliete quante più informazioni possibili – screenshot, rapporti di crash, log, link (per i servizi web), ecc. Vorrete che l’utente che ha riscontrato il problema fornisca informazioni dettagliate sull’evento, tra cui: quando e dove si è verificato il problema nel programma, le sue azioni che hanno portato al problema, l’ambiente operativo e qualsiasi comportamento strano o insolito del programma prima e dopo che si è verificato il problema.

Da qui, potete iniziare a raccogliere informazioni sui log. Se la vostra applicazione è un servizio ospitato, iniziate a recuperare i log dal web e dal server dell’applicazione. Se la vostra applicazione è un pacchetto software distribuito, chiedete all’utente di includere i dati di log nel suo rapporto di bug. In alternativa, se la vostra applicazione invia eventi di log ad un server di log centralizzato, allora i vostri log saranno immediatamente disponibili.

Una volta che avete una quantità ragionevole di dati riguardanti il problema, potete iniziare a rintracciarlo nel codice.

Identificare la causa del problema

Dopo aver raccolto informazioni sul problema, il passo successivo è identificare la causa. Riprodurre un bug in un ambiente di sviluppo è uno dei modi più semplici per convalidarne l’esistenza, ma può richiedere molto tempo e potrebbe non funzionare in tutti i casi. Avere un set completo di log vi porterà direttamente alla fonte del problema, risparmiandovi tempo e frustrazione.

Un rapporto di bug vi darà un’idea generale di quale sia il problema e dove si sia verificato. Usando lo strumento di gestione dei log che avete scelto, potete restringere la vostra ricerca a una gamma più piccola di voci di log cercando un token di dati unico come un nome utente, un ID di sessione o il testo del messaggio.

Facciamo un esempio di scenario per dimostrare come eseguire il debug di un sistema. Immaginiamo di avere un’interfaccia basata sul web per accedere da remoto a un sito web. La pagina web ha una schermata di login che esegue l’autenticazione di base utilizzando un nome utente e una password. Gli utenti hanno segnalato che non possono accedere al sito web. La pagina web accetta il loro input ma poi fallisce con un errore generico.

Un sito web di esempio che riporta un errore dopo un tentativo di login.

Questo messaggio non ci dà molte informazioni se non una generica indicazione della gravità del log. Cercare in un log manager le voci con “Severe” nel livello o nel messaggio potrebbe portare a centinaia di risultati senza alcuna garanzia che nessuno di essi sia collegato al problema in questione. Fortunatamente, il nostro Loggerha anche registrato il nome utente dell’utente che ha sperimentato l’errore, così possiamo filtrare sul nome utente “admin”.

Visualizzare un’eccezione Java in Loggly.

Se facciamo il drill down, vediamo che la causa dell’errore è una tabella mancante o non valida. Questo è un problema serio, poiché potrebbe indicare una cancellazione accidentale o una corruzione del database.

ERROR: Exception for user admincom.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'test_schema.users' doesn't exist...com.mysql.jdbc.StatementImpl.executeQuery(StatementImpl.java:1651) at TestApplication.Test.doPost(Test.java:33) at javax.servlet.http.HttpServlet.service(HttpServlet.java:646) at javax.servlet.http.HttpServlet.service(HttpServlet.java:727) at...

Guardando lo stack trace, vediamo che il servizio è fallito alla linea 33 di Test.java. Questa linea consiste in un’istruzione SQL che estrae i dati di un utente dalla tabella test_schema.users.

rs = stmt.executeQuery("SELECT * FROM test_schema.users WHERE username = " + username + " AND Password = " + password);

Quando proviamo a eseguire questa query in un front end SQL, troviamo che la tabella “test_schema.users” non esiste. Tuttavia, il database ha una tabella “test_schema.user”. Ad un certo punto, uno sviluppatore potrebbe aver erroneamente digitato il nome sbagliato della tabella e spinto il modulo modificato in produzione. Ora sappiamo qual è il problema e dove appare nel nostro codice.

Usando SolarWinds Loggly, è possibile eseguire il debug e trovare la causa principale dei problemi tracciando gli errori attraverso lo stack dell’applicazione, attraverso più eventi di log e persino individuando la riga di codice rilevante in GitHub.

Risolvere il problema e impedire che si ripresenti

Ora che abbiamo identificato il problema e la sua causa, il passo finale è quello di risolverlo. Il nostro esempio di login era un caso esagerato con una soluzione relativamente facile, ma potreste imbattervi in bug più complicati radicati in diverse aree della vostra applicazione. Prima di saltare per la soluzione rapida e sporca, considerate attentamente come il vostro cambiamento avrà un impatto sull’applicazione. È davvero la soluzione migliore per il problema? È possibile che il vostro cambiamento interferisca con un altro componente? Questa correzione renderà difficile l’introduzione di nuove correzioni o caratteristiche più avanti nel tempo? Questa correzione impedirà anche che problemi simili si presentino in seguito?

Per esempio, cosa succede se due diversi utenti riescono a creare due account separati con lo stesso nome utente e password? Potreste imporre un nome utente unico per tutti gli utenti, ma che effetto avrebbe sulla struttura del vostro database, sul vostro codice di autenticazione e sulla vostra base di utenti esistente? Potreste aggiungere un nuovo campo unico come un indirizzo e-mail e renderlo obbligatorio, ma cosa succede se alcuni dei vostri attuali utenti non hanno fornito indirizzi e-mail? E se alcuni indirizzi e-mail sono condivisi tra più account? Come influiranno queste nuove regole sulla registrazione, la ricerca e le pagine di amministrazione?

Vorrete anche considerare il ritardo tra la creazione di una correzione e la sua distribuzione. In un ambiente aziendale, il vostro codice dovrà essere rivisto da altri sviluppatori, integrato nella base di codice, costruito, testato da QA, messo in scena e forse passare attraverso molti altri passi prima di andare in produzione. La vostra azienda potrebbe avere dei protocolli specifici per i bug critici o sensibili al tempo che dovete seguire. Una volta che avete trovato una soluzione, includete la vostra risoluzione nella vostra documentazione, così come ogni ragionevole soluzione al problema. In questo modo, i vostri clienti possono ancora utilizzare il vostro prodotto e il vostro team di supporto può ridurre il numero di segnalazioni di bug duplicati mentre la soluzione si fa strada verso la produzione.

Dopo aver distribuito la soluzione, continuate a monitorare la vostra applicazione in produzione per verificare che il problema sia stato risolto. Per esempio, dopo aver distribuito la correzione per il problema del database nell’esempio precedente, non dovremmo più vedere eventi con “La tabella ‘test_schema.users’ non esiste”. Se gli errori continuano o se iniziamo a vedere nuovi errori, sappiamo che la correzione non ha funzionato. Idealmente, vedremo un modello come quello mostrato nel seguente screenshot, dove il numero di errori scende a zero subito dopo aver distribuito la patch.

Cartografia del numero di log contenenti un errore in Loggly.

Altri strumenti di debug di produzione

Mentre il logging è il metodo collaudato per la risoluzione dei problemi e il debug delle applicazioni, questi strumenti possono aiutarvi a ottenere maggiori informazioni sul funzionamento della vostra applicazione.

jdb

jdb, il Java Debugger, è un’utilità a riga di comando per il debug delle classi Java. jdb è facile da usare e viene fornito con il Java Development Kit. Usando jdb si crea una nuova Java Virtual Machine (JVM), permettendovi di eseguire il debug di una classe senza influenzare alcun programma in esecuzione. Puoi anche usare jdb per fare il debug delle applicazioni in esecuzione aggiungendo i seguenti parametri al tuo comando java:

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n

Quando la JVM si avvia, assegna un numero di porta per le connessioni jdb in entrata. Puoi quindi collegarti all’istanza della JVM in esecuzione usando jdb -attach:

$ jdb -attach

Puoi usarlo, per esempio, per collegare il tuo debugger a un’istanza di produzione in esecuzione. Fate attenzione all’uso dei breakpoint in questo scenario, poiché potrebbe mettere in pausa un thread attivo. Questo può avere conseguenze se, per esempio, un cliente sta usando l’applicazione mentre la state debuggando. Per maggiori informazioni, vedi la documentazione Java su jdb.

OverOps

OverOps è una suite di strumenti per monitorare le applicazioni, analizzare il codice e rilevare i problemi. A differenza del software di registrazione, che si basa sull’output generato da un’applicazione in esecuzione, OverOps si connette direttamente alla Java Virtual Machine per mappare il codice base dell’applicazione, leggere le variabili e registrare gli errori. Questo gli permette di catturare più errori e registrare più dati rispetto al framework di registrazione dell’applicazione. OverOps utilizza un modello SaaS (software as a service), dove le metriche sono raccolte e memorizzate sui server cloud di OverOps. Tuttavia, è anche possibile distribuirlo on-premises. In entrambi i casi, è possibile visualizzare i dati utilizzando un’interfaccia basata sul web.

BTrace

BTrace è uno strumento di tracciamento che consente di monitorare tutti gli aspetti della vostra applicazione, dai nomi delle classi agli errori. BTrace usa un approccio di programmazione orientato agli aspetti che coinvolge l’uso di annotazioni, che specificano dove e come BTrace monitorizza la tua applicazione. Per esempio, il seguente script BTrace monitora e registra ogni singola chiamata al pacchetto javax.swing.

import com.sun.btrace.annotations.*;import static com.sun.btrace.BTraceUtils.*; @BTrace public class AllMethods { @OnMethod( clazz="/javax\.swing\..*/", method="/.*/" ) public static void m(@ProbeClassName String probeClass, @ProbeMethodName String probeMethod) { print(Strings.strcat("entered ", probeClass)); println(Strings.strcat(".", probeMethod)); }}

Per maggiori informazioni su BTrace, vedi il repository GitHub e il Wiki di BTrace.

Chronon

Chronon ti permette di riavvolgere e riprodurre l’intero flusso di esecuzione di un’applicazione. Registra ogni singolo cambiamento fatto durante la vita di un’applicazione, permettendovi di riprodurre lo stato dell’applicazione in qualsiasi momento. Le registrazioni vengono salvate su file, rendendo facile il trasferimento della cronologia di esecuzione da una macchina di produzione a una macchina di sviluppo per i test.

Chronon è composto da Chronon Recording Server, che consente di registrare applicazioni Java in remoto; Embedded Chronon, che incorpora il registratore all’interno di un’applicazione; e il Time Travelling Debugger, che consente di riprodurre le registrazioni.

jhsdb

jhsdb (Java HotSpot Debugger) è una suite di strumenti per il debug, l’analisi e il profiling della JVM predefinita fornita con OpenJDK e Oracle JDK. jhsdb permette di collegarsi ai processi Java in esecuzione, prendere istantanee delle tracce dello stack e persino analizzare le JVM in crash. Potete usarlo per accedere all’heap, alla cache del codice, alle statistiche di garbage collection e altro. Per saperne di più, consultate la pagina di documentazione di jhsdb.

Tracing delle transazioni

Quando si verifica un problema, è importante sapere dove il problema è iniziato e come ha influenzato il resto della vostra applicazione. Questo è abbastanza difficile in un’applicazione monolitica, ma diventa ancora più difficile in un’architettura distribuita orientata ai servizi, dove una singola richiesta può colpire decine di servizi. Non è sempre ovvio quale servizio contenga la causa principale dell’errore, o come abbia influenzato gli altri servizi. Il tracciamento fornisce i dati necessari per seguire il percorso di esecuzione dell’applicazione e individuare la causa esatta di un problema.

Nella sezione Debugging Production Problems della guida, abbiamo fatto un esempio in cui un utente aveva difficoltà ad accedere a un’applicazione web a causa di una tabella di database non valida. In questa sezione, mostreremo come il tracciamento delle transazioni abbia giocato un ruolo centrale nella risoluzione del problema.

Tracciare gli ID unici

Per tracciare una sequenza di eventi di log, è necessario un modo per identificare in modo univoco i log correlati. Un ambiente multiutente potrebbe generare centinaia di log identici, rendendo difficile la ricerca basata sul timestamp o Logger. Una soluzione più semplice è quella di includere un identificatore unico con le voci di log correlate. Questo identificatore potrebbe essere un nome utente, un ID di sessione, una chiave API o un UUID (Universally Unique Identifier). Fortunatamente, ThreadContext è perfettamente adatto per questo lavoro.

Nell’esempio del debug dei problemi di produzione, avevamo un’interfaccia utente basata sul web che era ospitata in un servlet Tomcat, che si collegava a un database MySQL. Gli utenti inserivano le loro credenziali sulla pagina web, e dopo aver premuto invio, il servlet eseguiva una query confrontando le loro credenziali con quelle memorizzate nel database. Se l’utente veniva autenticato con successo, allora veniva reindirizzato alla pagina principale. Se si verificava un errore, i dettagli dell’errore venivano registrati e gli utenti venivano presentati con un messaggio generico.

Siamo stati in grado di risolvere questo problema includendo i nomi utente nel messaggio di log. Questo ci ha permesso di cercare rapidamente gli eventi di log relativi all’utente admin. Partendo da questo stesso esempio, possiamo usare ThreadContext.put() per mappare un nome utente su un Logger. Quando gli utenti inviano le loro credenziali, il servlet entra nel metodo doPost(), che aggiunge i nomi utente al ThreadContext:

public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {ThreadContext.put("username", request.getParameter("username"));logger.info("Entering doPost().");...}

Utilizzando Log4j con lo schema %p : %m%n si ottiene la seguente voce:

INFO : Entering doPost().

Possiamo aggiungere eventi simili per accedere al database MySQL, lasciare il metodo doPost, ed eseguire altre azioni. In questo modo, se l’utente innesca un’eccezione, sappiamo esattamente cosa stava facendo l’utente quando si è verificata l’eccezione.

Tracing delle chiamate al metodo

Molti framework di log forniscono metodi nativi per tracciare il percorso di esecuzione di un’applicazione. Questi metodi variano leggermente tra i framework, ma seguono lo stesso formato generale.

  • traceEntry() segna l’inizio di un metodo.
  • traceExit() segna la fine di un metodo. Per i metodi che restituiscono un oggetto, potete contemporaneamente restituire l’oggetto e registrare l’evento con return logger.exit(object).
  • throwing() contrassegna un’eccezione che difficilmente verrà gestita, come una RuntimeException.
  • catching() contrassegna un’eccezione che non verrà rilanciata.

Potete trovare informazioni specifiche del framework su questi metodi nella documentazione dei logger per Log4j, Logback e java.util.logging.

Metodi di tracciamento in Log4j

Come esempio di utilizzo di Log4j, sostituiremo i metodi Logger.info() nella nostra servlet con metodi di tracciamento.

public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { logger.entry(); ... logger.exit();}

Cambieremo anche il Appender's PatternLayout per mostrare il nome della classe, il metodo e il numero di linea (i modelli di conversione %class, %M e %line).

<PatternLayout pattern="%p %class %M %line: %m%n" />

I metodi di tracciamento registrano gli eventi al livello TRACE, il che significa che dobbiamo cambiare il livello Logger's da DEBUG a TRACE. Altrimenti, i messaggi di log saranno soppressi.

<Loggers> <Root level="trace"> <AppenderRef ref="Console"/> </Root></Loggers> TRACE DatabaseApplication.Login doPost 26: entry...TRACE DatabaseApplication.Login doPost 59: exit

I metodi di tracciamento forniscono anche i propri Markers. Per esempio, i metodi Logger.entry() e Logger.exit() visualizzano rispettivamente ENTER e EXIT.

ENTER 14:47:41.074 TRACE DatabaseApplication.Login - EnterEXIT 14:47:41.251 TRACE DatabaseApplication.Login - Exit

Metodi di tracciamento in SLF4J

Gli utenti SLF4J possono sfruttare MDC per tracciare gli eventi di log. Similmente a Log4j, i valori MDC possono essere usati con un Appender usando il modello di conversione %X.

SLF4J fornisce anche metodi entry(), exit(), throwing() e catching() attraverso la classe XLogger (Extended Logger). Puoi creare un’istanza di XLogger usando XLoggerFactory.getXLogger().

package DatabaseApplication;import org.slf4j.Logger;import org.slf4j.LoggerFactory; import org.slf4j.ext.XLogger;import org.slf4j.ext.XLoggerFactory; public class Login extends HTTPServlet { final static XLogger xlogger = XLoggerFactory.getXLogger(Login.class.getName()); final static Logger logger = LoggerFactory.getLogger(Login.class.getName()); @Override public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { xlogger.entry(); .. xlogger.exit(); }}

Aggiungi i pattern di conversione %class e %line a logback.xml.

<configuration> <appender name="Console" class="ch.qos.Logback.core.ConsoleAppender"> <encoder> <pattern>%-5level %class{36} %M %L: %msg%xEx%n</pattern> </encoder> </appender> <root level="trace"> ... </root></configuration>

Le voci di log risultanti sono identiche a quelle create da Log4j.

Gestire l’uso della memoria

La gestione della memoria è spesso trascurata nei linguaggi di livello superiore come Java. Mentre la quantità media di memoria nei dispositivi moderni sta aumentando, un uso elevato della memoria può avere un grande impatto sulla stabilità e sulle prestazioni della vostra applicazione. Una volta che la Java Virtual Machine non può più allocare memoria dal sistema operativo, il vostro programma potrebbe terminare e andare in crash. Sapere come gestire la memoria eviterà problemi man mano che la vostra applicazione cresce in dimensioni.

Per esempio, immaginiamo di voler memorizzare una serie di numeri a partire da uno. Un utente fornisce un limite superiore e il programma memorizza ogni numero intero da “1” a quel limite. Useremo un ciclo while con un contatore per aggiungere ogni numero a un array.

import java.util.Scanner;...System.out.print("Please enter the maximum size of the array: ");Scanner scanner = new Scanner(System.in);int limit = scanner.nextInt(); ArrayList intArray = new ArrayList(); int count = 1;while (count <= limit) { intArray.add(count);}

Avrete notato che count non aumenta nel ciclo. Questo è un grosso problema; se l’utente inserisce un qualsiasi numero maggiore di zero, l’array continuerà a crescere fino a quando la JVM userà tutta la sua memoria disponibile e il programma andrà in crash.

Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceat java.util.Arrays.copyOf(Arrays.java:2245)at java.util.Arrays.copyOf(Arrays.java:2219)at java.util.ArrayList.grow(ArrayList.java:242)at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216)at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208)at java.util.ArrayList.add(ArrayList.java:440)at MemoryTest.main(MemoryTest.java:10)

Garbage Collection

Per ridurre l’uso della memoria, la Java Virtual Machine esegue un processo periodico di pulizia noto come garbage collection. Garbage collection cerca blocchi di memoria non più in uso e li rende disponibili per il riutilizzo. Le seguenti risorse spiegano il processo di garbage collection di Java in modo più dettagliato.

  • Java Garbage Collection Basics (Oracle)
  • HotSpot Virtual Machine Garbage Collection Tuning Guide (Oracle)
  • Understanding G1 GC Logs (Poonam Bajaj)
  • Garbage Collection Tuning Guide (Atlassian)

Il garbage collector genera informazioni diagnostiche che possono essere utili per il profiling delle applicazioni. Puoi registrare queste informazioni passando -Xlog:gc alla JVM all’avvio dell’applicazione. Dopo ogni esecuzione, il garbage collector stampa le statistiche sul processo di garbage collection nel formato Unified JVM Logging. Ecco un esempio.

 Using G1 Periodic GC disabled GC(0) Pause Young (Normal) (G1 Evacuation Pause) 7M->1M(64M) 8.450ms

Using G1 dice quale metodo di garbage collection è in uso. Il collettore Garbage-First (G1) è spesso abilitato di default sui computer multiprocessore con grandi quantità di RAM. Periodic GC disabled indica che il processo di garbage collection non si ripeterà. La terza linea ci dice che questa è una pausa di evacuazione, dove gli oggetti vengono copiati tra le regioni di memoria in base al fatto che siano ancora in uso. Pause Young ci dice che il processo ha pulito la generazione giovane, che è dove vengono allocati nuovi oggetti. Come risultato, l’utilizzo totale della memoria da parte degli oggetti nella generazione giovane è sceso da 7M a 1M su 64M allocati, e il processo ha impiegato 8.450ms.

Con il garbage collector che stampa informazioni diagnostiche sulla console, possiamo vedere come la quantità di memoria allocata (la dimensione dello heap) continua a crescere nel tempo. Rimuoveremo i tempi della CPU per semplicità.

$ java -Xlog:gc MemoryLeakDemo Using G1 Periodic GC disabled GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) 25M->23M(64M) 4.372ms GC(1) Concurrent Cycle GC(1) Pause Remark 50M->34M(64M) 0.250ms GC(1) Pause Cleanup 94M->94M(124M) 0.057ms GC(1) Concurrent Cycle 169.695ms GC(2) Pause Young (Concurrent Start) (G1 Humongous Allocation) 94M->94M(124M) 3.738ms GC(3) Concurrent Cycle...

Grafico che mostra un graduale aumento dell’utilizzo dell’heap della JVM nel tempo.

Puoi aggiungere l’indicazione del tempo ad ogni voce con -XX:+PrintGCDateStamps e -XX:+PrintGCTimeStamps. Potete anche registrare l’output del garbage collector in un file passandoXlog:gc:file: alla JVM all’avvio della vostra applicazione.

Se monitorate i vostri file di log con rsyslog, potete poi inoltrare i log ad un sistema di log centralizzato dove saranno analizzati e pronti per un’analisi quasi in tempo reale. Ecco come appare un grafico temporale in Loggly. Mostra la dimensione totale dell’heap, così come la dimensione dell’heap prima e dopo l’esecuzione del garbage collector.

Memory Leaks

Un memory leak è una situazione in cui un programma alloca memoria più velocemente di quanto possa rilasciarla. Il modo più semplice per rilevare una perdita di memoria è quando il tuo programma non risponde, diventa instabile o causa OutOfMemoryErrors. In Java, troverai più istanze di collezioni Full GC man mano che l’uso della memoria aumenta.

Puoi saperne di più sull’output del garbage collector e sull’individuazione delle perdite di memoria nel capitolo Troubleshoot Memory Leaks della documentazione Java.

Risorse aggiuntive

Tagliare il tempo del debug senza scrivere una sola riga di codice (Loggly)-Guida all’uso di Takipi

The Ultimate Guide: 5 Methods for Debugging Production Servers at Scale (High Scalability)-Tools and techniques for debugging production problems

Memory Management

Garbage Collection Tuning Guide (Oracle)-Guida alla comprensione e messa a punto della garbage collection di Java

Understanding Java Garbage Collection (CUBRID Blog)-Guida alla garb

See it. Analizzalo. Ispeziona. Risolvilo

Vedi ciò che conta.

INIZIA LA PROVA GRATUITA

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.