Verbose Garbage Collection în Java

1. Prezentare generală

În acest tutorial, vom arunca o privire asupra modului de activare a colectării verbale a gunoiului într-o aplicație Java . Vom începe prin a introduce ce este colectarea de gunoaie detaliată și de ce poate fi utilă.

În continuare, vom analiza câteva exemple diferite și vom afla despre diferitele opțiuni de configurare disponibile. În plus, ne vom concentra și asupra modului de interpretare a rezultatelor jurnalelor noastre detaliate.

Pentru a afla mai multe despre Garbage Collection (GC) și diferitele implementări disponibile, consultați articolul nostru despre Java Garbage Collectors.

2. Scurtă introducere în colectarea deșeurilor verbale

Activarea jurnalului de colectare a gunoiului detaliat este adesea necesară atunci când reglați și depanați multe probleme , în special probleme de memorie. De fapt, unii ar argumenta că, pentru a monitoriza cu strictețe starea aplicației noastre, ar trebui să monitorizăm întotdeauna performanța JVM's Garbage Collection.

După cum vom vedea, jurnalul GC este un instrument foarte important pentru a dezvălui potențiale îmbunătățiri ale heap-ului și configurației GC a aplicației noastre. Pentru fiecare GC care se întâmplă, jurnalul GC oferă date exacte despre rezultatele și durata acestuia.

În timp, analiza acestor informații ne poate ajuta să înțelegem mai bine comportamentul aplicației noastre și să ne ajute să ajustăm performanța aplicației noastre. Mai mult, poate ajuta la optimizarea frecvenței GC și a timpilor de colectare prin specificarea celor mai bune dimensiuni de heap, alte opțiuni JVM și algoritmi GC alternativi.

2.1. Un program Java simplu

Vom folosi un program Java simplu pentru a demonstra cum să activăm și să interpretăm jurnalele noastre GC:

public class Application { private static Map stringContainer = new HashMap(); public static void main(String[] args) { System.out.println("Start of program!"); String stringWithPrefix = "stringWithPrefix"; // Load Java Heap with 3 M java.lang.String instances for (int i = 0; i < 3000000; i++) { String newString = stringWithPrefix + i; stringContainer.put(newString, newString); } System.out.println("MAP size: " + stringContainer.size()); // Explicit GC! System.gc(); // Remove 2 M out of 3 M for (int i = 0; i < 2000000; i++) { String newString = stringWithPrefix + i; stringContainer.remove(newString); } System.out.println("MAP size: " + stringContainer.size()); System.out.println("End of program!"); } }

După cum putem vedea în exemplul de mai sus, acest program simplu încarcă 3 milioane de instanțe String într-un obiect Map . Apoi facem un apel explicit către colectorul de gunoi folosind System.gc () .

În cele din urmă, eliminăm 2 milioane de instanțe String din hartă . De asemenea, folosim în mod explicit System.out.println pentru a facilita interpretarea rezultatului.

În secțiunea următoare, vom vedea cum se activează înregistrarea GC.

3. Activarea înregistrării GC „simple”

Să începem prin rularea programului nostru și activarea GC detaliată prin intermediul argumentelor noastre de pornire JVM:

-XX:+UseSerialGC -Xms1024m -Xmx1024m -verbose:gc

Argumentul important aici este -verbose: gc , care activează înregistrarea informațiilor de colectare a gunoiului în cea mai simplă formă . În mod implicit, jurnalul GC este scris în stdout și ar trebui să afișeze o linie pentru fiecare GC de generație tânără și pentru fiecare GC complet.

În scopul exemplului nostru, am specificat colectorul de gunoi serial, cea mai simplă implementare GC, prin argumentul -XX: + UseSerialGC .

De asemenea, am stabilit o dimensiune minimă și maximă a heap-ului de 1024 mb, dar există, desigur, mai mulți parametri JVM pe care îi putem regla.

3.1. Înțelegerea de bază a ieșirii verbale

Acum să aruncăm o privire asupra rezultatului programului nostru simplu:

Start of program! [GC (Allocation Failure) 279616K->146232K(1013632K), 0.3318607 secs] [GC (Allocation Failure) 425848K->295442K(1013632K), 0.4266943 secs] MAP size: 3000000 [Full GC (System.gc()) 434341K->368279K(1013632K), 0.5420611 secs] [GC (Allocation Failure) 647895K->368280K(1013632K), 0.0075449 secs] MAP size: 1000000 End of program!

În rezultatul de mai sus, putem vedea deja o mulțime de informații utile despre ceea ce se întâmplă în interiorul JVM.

La început, această ieșire poate părea destul de descurajantă, dar hai să o parcurgem pas cu pas.

În primul rând, putem vedea că au avut loc patru colecții, una GC completă și trei generații tinere de curățenie.

3.2. Ieșirea Verbose în mai multe detalii

Să descompunem liniile de ieșire mai detaliat pentru a înțelege exact ce se întâmplă:

  1. GC sau completa GC - Tipul de gunoi de colectare, fie GC sau completa GC pentru a distinge un minor sau de colectare a gunoiului plin
  2. (Eroare de alocare) sau (System.gc ()) - Cauza colecției - Eroare de alocare indică faptul că nu a mai rămas spațiu în Eden pentru a ne aloca obiectele
  3. 279616K-> 146232K - Memoria heap ocupată înainte și după GC, respectiv (separate printr-o săgeată)
  4. (1013632K) - Capacitatea curentă a heap-ului
  5. 0.3318607 secunde - Durata evenimentului GC în secunde

Astfel, dacă luăm prima linie, 279616K-> 146232K (1013632K) înseamnă că GC a redus memoria heap ocupată de la 279616K la 146232K . Capacitatea heap la momentul GC era 1013632K , iar GC a durat 0,3318607 secunde.

Cu toate acestea, deși formatul simplu de înregistrare GC poate fi util, acesta oferă detalii limitate. De exemplu, nu putem spune dacă GC a mutat obiecte de la generația tânără la cea veche sau care a fost dimensiunea totală a generației tinere înainte și după fiecare colecție .

Din acest motiv, înregistrarea detaliată a GC este mai utilă decât cea simplă.

4. Activarea înregistrării GC „detaliate”

Pentru a activa înregistrarea detaliată a GC, folosim argumentul -XX: + PrintGCDetails . Acest lucru ne va oferi mai multe detalii despre fiecare GC, cum ar fi:

  • Dimensiunea generației tinere și bătrâne înainte și după fiecare CG
  • Timpul necesar pentru ca un GC să se întâmple în generația tânără și cea veche
  • Dimensiunea obiectelor promovate la fiecare GC
  • Un rezumat al dimensiunii heap-ului total

În exemplul următor, vom vedea cum să capturăm informații și mai detaliate în jurnalele noastre, combinând -verbose: gc cu acest argument suplimentar.

Vă rugăm să rețineți că semnalizatorul -XX: + PrintGCDetails a fost învechit în Java 9, în favoarea noului mecanism de înregistrare unificat (mai multe despre acest lucru mai târziu). Oricum, noul echivalent al -XX: + PrintGCDetails este opțiunea -Xlog: gc * .

5. Interpretarea ieșirii verbale „detaliate”

Să rulăm din nou programul nostru de probă:

-XX:+UseSerialGC -Xms1024m -Xmx1024m -verbose:gc -XX:+PrintGCDetails

De data aceasta rezultatul este destul de mai detaliat:

Start of program! [GC (Allocation Failure) [DefNew: 279616K->34944K(314560K), 0.3626923 secs] 279616K->146232K(1013632K), 0.3627492 secs] [Times: user=0.33 sys=0.03, real=0.36 secs] [GC (Allocation Failure) [DefNew: 314560K->34943K(314560K), 0.4589079 secs] 425848K->295442K(1013632K), 0.4589526 secs] [Times: user=0.41 sys=0.05, real=0.46 secs] MAP size: 3000000 [Full GC (System.gc()) [Tenured: 260498K->368281K(699072K), 0.5580183 secs] 434341K->368281K(1013632K), [Metaspace: 2624K->2624K(1056768K)], 0.5580738 secs] [Times: user=0.50 sys=0.06, real=0.56 secs] [GC (Allocation Failure) [DefNew: 279616K->0K(314560K), 0.0076722 secs] 647897K->368281K(1013632K), 0.0077169 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] MAP size: 1000000 End of program! Heap def new generation total 314560K, used 100261K [0x00000000c0000000, 0x00000000d5550000, 0x00000000d5550000) eden space 279616K, 35% used [0x00000000c0000000, 0x00000000c61e9370, 0x00000000d1110000) from space 34944K, 0% used [0x00000000d3330000, 0x00000000d3330188, 0x00000000d5550000) to space 34944K, 0% used [0x00000000d1110000, 0x00000000d1110000, 0x00000000d3330000) tenured generation total 699072K, used 368281K [0x00000000d5550000, 0x0000000100000000, 0x0000000100000000) the space 699072K, 52% used [0x00000000d5550000, 0x00000000ebcf65e0, 0x00000000ebcf6600, 0x0000000100000000) Metaspace used 2637K, capacity 4486K, committed 4864K, reserved 1056768K class space used 283K, capacity 386K, committed 512K, reserved 1048576K

Ar trebui să putem recunoaște toate elementele din jurnalul GC simplu. Dar există mai multe elemente noi.

Să luăm acum în considerare noile elemente din ieșire care sunt evidențiate cu albastru în secțiunea următoare:

5.1. Interpretarea unui GC minor în Young Generation

Vom începe prin analiza noilor piese într-un GC minor:

  • [GC (Allocation Failure) [DefNew: 279616K->34944K(314560K), 0.3626923 secs] 279616K->146232K(1013632K), 0.3627492 secs] [Times: user=0.33 sys=0.03, real=0.36 secs]

As before we'll break the lines down into parts:

  1. DefNew – Name of the garbage collector used. This not so obvious name stands for the single-threaded mark-copy stop-the-world garbage collector and is what is used to clean the Young generation
  2. 279616K->34944K – Usage of the Young generation before and after collection
  3. (314560K) – The total size of the Young generation
  4. 0.3626923 secs – The duration in seconds
  5. [Times: user=0.33 sys=0.03, real=0.36 secs] – Duration of the GC event, measured in different categories

Now let's explain the different categories:

  • user – The total CPU time that was consumed by Garbage Collector
  • sys – The time spent in OS calls or waiting for system events
  • real – This is all elapsed time including time slices used by other processes

Since we're running our example using the Serial Garbage Collector, which always uses just a single thread, real-time is equal to the sum of user and system times.

5.2. Interpreting a Full GC

In this penultimate example, we see that for a major collection (Full GC), which was triggered by our system call, the collector used was Tenured.

The final piece of additional information we see is a breakdown following the same pattern for the Metaspace:

[Metaspace: 2624K->2624K(1056768K)], 0.5580738 secs]

Metaspace is a new memory space introduced in Java 8 and is an area of native memory.

5.3. Java Heap Breakdown Analysis

The final part of the output includes a breakdown of the heap including a memory footprint summary for each part of memory.

We can see that Eden space had a 35% footprint and Tenured had a 52% footprint. A summary for Metadata space and class space is also included.

From the above examples, we can now understand exactly what was happening with memory consumption inside the JVM during the GC events.

6. Adding Date and Time Information

No good log is complete without date and time information.

This extra information can be highly useful when we need to correlate GC log data with data from other sources, or it can simply help facilitate searching.

We can add the following two arguments when we run our application to get date and time information to appear in our logs:

-XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps

Each line now starts with the absolute date and time when it was written followed by a timestamp reflecting the real-time passed in seconds since the JVM started:

2018-12-11T02:55:23.518+0100: 2.601: [GC (Allocation ...

Please note that these tuning flags have been removed in Java 9. The new alternative is:

-Xlog:gc*::time

7. Logging to a File

As we've already seen, by default the GC log is written to stdout. A more practical solution is to specify an output file.

We can do this by using the argument -Xloggc: where file is the absolute path to our output file:

-Xloggc:/path/to/file/gc.log

Similar to other tuning flags, Java 9 deprecated the -Xloggc flag in favor of the new unified logging. To be more specific, now the alternative for logging to a file is:

-Xlog:gc:/path/to/file/gc.log

8. Java 9: Unified JVM Logging

As of Java 9, most of the GC related tuning flags have been deprecated in favor of the unified logging option -Xlog:gc. The verbose:gc option, however, still works in Java 9 and newer version.

For instance, as of Java 9, the equivalent of the -verbose:gc flag in the new unified logging system is:

-Xlog:gc

This will log all the info level GC logs to the standard output. It's also possible to use the -Xlog:gc= syntax to change the log level. For instance, to see all debug level logs:

-Xlog:gc=debug

As we saw earlier, we can change the output destination via the -Xlog:gc=: syntax. By default, the output is stdout, but we can change it to stderr or even a file:

-Xlog:gc=debug:file=gc.txt

Also, it's possible to add a few more fields to the output using decorators. For instance:

-Xlog:gc=debug::pid,time,uptime

Here we're printing the process id, uptime, and current timestamp in each log statement.

To see more examples of the Unified JVM Logging, see the JEP 158 standard.

9. A Tool to Analyze GC Logs

It can be time-consuming and quite tedious to analyze GC logs using a text editor. Depending on the JVM version and the GC algorithm that is used, the GC log format could differ.

Există un instrument foarte bun de analiză grafică gratuită care analizează jurnalele de colectare a gunoiului, oferă multe valori despre potențiale probleme de colectare a gunoiului și chiar oferă soluții potențiale la aceste probleme.

Verificați cu siguranță Universal GC Log Analyzer!

10. Concluzie

Pentru a rezuma, în acest tutorial, am explorat în detaliu colectarea detaliată a gunoiului în Java.

În primul rând, am început prin a introduce ce este colectarea de gunoaie detaliată și de ce am putea dori să o folosim. Am analizat apoi câteva exemple folosind o aplicație Java simplă. Am început cu activarea înregistrării GC în forma sa cea mai simplă înainte de a explora câteva exemple mai detaliate și cum să interpretăm rezultatul.

În cele din urmă, am explorat mai multe opțiuni suplimentare pentru înregistrarea informațiilor privind data și ora și modul de scriere a informațiilor într-un fișier jurnal.

Exemplele de coduri pot fi găsite pe GitHub.