Turning on the GC log is easy. Reading it is harder: one G1 line holds about ten pieces of information, and the words change from one Java version to the next.
This post reads real lines, written by three small programs of the public repository java-gc-lab: a normal load, very big objects, and a heap that is almost full.
Turn on the log
java -Xlog:gc*:file=gc.log:time,uptime,level,tags -jar app.jar
gc*: all the GC messages (gcalone gives one line per GC);file=gc.log: in a file, with rotation built in (5 files of about 20 MB by default);time,uptime,level,tags: the “decorations” at the start of each line: the date and time, the time since the JVM started, the level and the tags. If you set nothing, the JVM usesuptime,levelandtags.
A pause line, with these decorations:
[2026-10-22T23:23:46.842+0200][0.356s][info][gc ] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 117M->24M(256M) 1.660ms
The exact time (time) helps to link a pause to an event of the
application, for example a slow request in another log.
One line, word by word
| Part | Meaning |
|---|---|
[0.469s] | the time since the JVM started (uptime) |
GC(1) | the number of the GC: all the lines of one GC have the same one |
Pause Young (Normal) | the type of pause: here a normal young pause. You also see Concurrent Start, Prepare Mixed, Mixed, Remark, Cleanup and Full |
(G1 Evacuation Pause) | the cause of the GC, here the usual one. Other causes: G1 Humongous Allocation, System.gc(), G1 Compaction Pause (full GC) |
117M->23M(256M) | the heap used before the GC, after the GC, and the heap size |
5.790ms | the length of the pause: the application is stopped during this time |
The words young, mixed and full are explained in the post about generations.
The 4 numbers to follow
Here are the start and the end of the log of NormalLoad.java, an application that keeps about 16 MB of data and creates short-lived objects all the time:
java -Xmx256m -Xlog:gc gc-logs/NormalLoad.java
[0.341s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 47M->18M(256M) 3.090ms
[0.469s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 117M->23M(256M) 5.790ms
[0.646s][info][gc] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 149M->24M(256M) 4.129ms
…
[3.240s][info][gc] GC(15) Pause Young (Normal) (G1 Evacuation Pause) 171M->24M(256M) 0.635ms
[3.478s][info][gc] GC(16) Pause Young (Normal) (G1 Evacuation Pause) 171M->24M(256M) 12.874ms
- The GC rate: 17 GCs in a little more than 3 seconds, about 5 per second. It goes up when the application creates more objects, or when the heap is too small.
- The pause time: from 0.4 to 12.9 ms here. Look at the longest ones first: they are the ones that users feel.
- The heap after the GC: 23 to 24 MB, stable. It is roughly the size of the live data. If it goes up from one GC to the next, it is a leak (see the post about OutOfMemoryError).
- The allocation rate: the heap before a GC, minus the heap after the previous GC, divided by the time between them. Between GC(0) and GC(1): (117 − 18) MB in 0.128 s, so about 770 MB/s. No line gives it directly: you have to compute it.
Warning signs
Humongous objects
java -Xmx256m -Xlog:gc gc-logs/Humongous.java
[0.306s][info][gc] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) 146M->9M(256M) 1.583ms
For G1, a “humongous” object is at least half a region. It does not go
through eden: it gets its own regions, directly in the old generation.
Here the program creates 2 MB arrays with 1 MB regions
(Heap Region Size: 1M at startup with -Xlog:gc*), and the
allocation itself starts the GC. Many G1 Humongous Allocation lines
point to big arrays or big buffers created often.
An evacuation failure, then Pause Full
java -Xmx64m -Xlog:gc gc-logs/NearTheLimit.java
[0.295s][info][gc] GC(5) Pause Young (Prepare Mixed) (G1 Evacuation Pause) (Evacuation Failure: Allocation) 61M->47M(64M) 0.250ms
[1.348s][info][gc] GC(175) Pause Full (G1 Compaction Pause) 62M->45M(64M) 5.691ms
The program keeps about 30 MB of data in a 64 MB heap. G1 no longer
finds enough room to copy the live objects: this is an evacuation
failure. The reason is Allocation (no room) or Pinned (an object
held in place by native code). If a GC frees nothing at all, G1 starts
a full GC, which compacts the whole heap and can be very slow.
Before Java 18: the evacuation failure was called
to-space exhausted. On Java 11 and 17, it was a separate line (GC(7) To-space exhausted); on Java 8, the end of the pause line. Java 21 writes(Evacuation Failure), and Java 25 adds the reason.
A Pause Full line with G1, when it does not come from System.gc(),
is always worth a look: the heap is too small for the live data, or
there is a leak.
Pauses that get longer
If the last number of the lines goes up over the hours, with the same type of pause, the GC has more and more work: more live data to copy, or a heap that fills up. Follow it closely.
The tools
- JFR (Java Flight Recorder), in the JDK: start it with
-XX:StartFlightRecording, and read the recording with JDK Mission Control. It shows the GC pauses with the rest of the application. More details in the post about tools. - Third-party tools also read the log files and draw charts, for example GCeasy (an online service) or GCViewer (open source).
What has changed
| What you still read | What is true today | Since |
|---|---|---|
-XX:+PrintGCDetails, -Xloggc:gc.log | -Xlog:gc*, -Xlog:gc*:file=gc.log (the old options are deprecated or refused, see the JVM options) | Java 9 |
to-space exhausted | Evacuation Failure, then Evacuation Failure: <reason> | Java 18 (JDK-8269022), the reason on Java 25 in my tests |
-XX:+PrintGCDateStamps to get the time | the time decoration of -Xlog | Java 9 |
Tested with Java 8 (Corretto 8u504), 11, 17, 21 and 25 (Temurin 11.0.32, 17.0.20, 21.0.9, 25.0.4), on macOS, with G1.
Sources: the documentation of the java command
(JDK 25, -Xlog), the
G1 chapter of Oracle’s GC guide
(humongous objects, evacuation failure, full GC),
JDK-8269022 (the new name
of the evacuation failure).