Abstract: How many times have we seen programmers call System.gc() to "help the garbage collector"? Unfortunately far too often, with potentially terrible performance implications. In this newsletter we explore the explicit and the diagnostic GC and how we can force a GC, even if explicit GCs are disabled.
Welcome to the 317th edition of The Java(tm) Specialists' Newsletter. My target for 2024 was to write a newsletter every month, preferably before the last day. But then today the weather on Crete was slighly overcast, but comfortably warm, and my wife's godsister Stasa came to visit with her husband and brother. We decided to spend the afternoon trying out a new taverna overlooking Tersanas Beach. It was perfect, with delicious fresh fish, and other well-prepared Cretan dishes. And so, our afternoon went by, and now I'm rushing to get this newsletter ready for you. I hope you enjoy it :-)
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
A few weeks ago, Kirk Pepperdine and I decided to create a recording of a course he wrote on solving Java memory leaks. It's super easy - look at the generational count to find the culprit, then start tracing the roots of these objects to find where they are leaking from.
Talking of leaks, here is a small story that happened to us recently. We live in the middle of nowhere and our water meter is far from our house. I opened our last water bill, and had to rub my eyes. Instead of our usual bill of $100-$200 for 3 months, I was staring of a figure of over $1000. How could this be? We try and economize our water usage as much as possible. In July 2022, our water line ran dry. We discovered that whilst cutting back some bushes, a farmer must have sawn through our pipe. He had dutifully stopped the leak by bending over the pipe. Then this year, during our Ascension Day church service, I noticed that the joint we had put in was leaking. As soon as we could, we repaired the leak, not knowing how much water had flowed into the sea. Months later the bill arrived. Fortunately, this being Crete, the water department was very understanding and wrote off a large part of that horrendous bill. In the meanwhile they had moved the water meter much closer to our house. We still have the occasional leak, but hopefully not $1000 worth. The lesson on leaks is: even a small leak will, over time, cause damage. Rather fix leaks!
Back to memory leaks. Some companies restart their servers every 24 hours, in order to "solve" their memory leaks. However, we feel that it is so easy to fix, that it is irresponsible to just leave them in our codebase. As I said above - look for the generational count to find the culprit, follow those types of objects to their GC roots, fix the leak - we're done. In case this does not seem obvious - we have recorded the course and you can buy it here. The course has two exercises with a detailed walkthrough. Since we recorded it with a live audience of two dozen of our JCretan friends, you also get the benefit of their questions and Kirk's answers.
During the course, Kirk was explaining the difference between
an explicit GC and a diagnostic GC. The explicit GC happens
when we call System.gc()
inside our code,
whereas the diagnostic GC is invoked through tooling. We can
turn off the explicit GC with
-XX:+DisableExplicitGC
, but we cannot turn off
the diagnostic GC. Kirk also mentioned that these events
are logged differently in the GC logs.
I do not know anyone who has looked at GC logs as closely as
Kirk, but I was sure that the tooling would simply call
System.gc()
and that the JVM setting
-XX:+DisableExplicitGC
would turn off these
calls. I thus wrote this little class to test it:
import java.io.*; import java.lang.management.*; public class DiagnosticVsExplicitGC { public static void main(String... args) throws IOException { // turn on verbose GC ManagementFactory.getMemoryMXBean().setVerbose(true); // create some objects for (int i = 0; i < 1000; i++) { byte[] object = new byte[100_000]; } // wait for input BufferedReader in = new BufferedReader( new InputStreamReader(System.in) ); System.out.println("Press ENTER to invoke explicit GC"); in.readLine(); System.gc(); System.out.println("Press ENTER to exit program"); in.readLine(); } }
When we run this code, we immediately see that the GC is run
due to the young space filling up. When we press ENTER,
the GC log lists that System.gc()
was called. Here is the
output:
java DiagnosticVsExplicitGC.java [0.297s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 79M->18M(1568M) 1.344ms [0.299s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 66M->18M(1568M) 0.833ms Press ENTER to invoke explicit GC [6.613s][info][gc] GC(2) Pause Full (System.gc()) 23M->18M(224M) 16.657ms Press ENTER to exit program
We can turn off the explicit GC with the flag
-XX:+DisableExplicitGC
, in which case the output
changes to:
java -XX:+DisableExplicitGC DiagnosticVsExplicitGC.java [0.223s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 79M->18M(1568M) 1.431ms [0.225s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 66M->18M(1568M) 0.870ms Press ENTER to invoke explicit GC Press ENTER to exit program
We have seen systems where they regularly call
System.gc()
in order to "help the garbage collector".
This is not necessary, and in most cases, is harmful. The
explicit GC typically triggers a stop-the-world full GC event,
which is one of the worst case scenarios for a JVM to
experience.
We can configure how our garbage collectors respond to an
explicit GC. Instead of doing a full GC, we can trigger the
start of a concurrent GC with
-XX:+ExplicitGCInvokesConcurrent
. This option
works for G1, and is on by default for Shenandoah.
Getting back to the difference between the explicit GC and
the diagnostic GC, let us have a look at what causes which
type of GC. We already established that
System.gc()
triggers the explicit GC, which may
be turned off, or which may start a concurrent GC instead a
full GC. In addition, when we click on "Perform GC" in the
Memory tab of JConsole, or the Monitor tab of VisualVM, the
explicit GC is invoked (which may be turned off). However,
when we call GC.run with the jcmd
tool, then
the "Diagnostic Command" is invoked. This ignores the
-XX:+DisableExplicitGC
flag, but honours the
-XX:+ExplicitGCInvokesConcurrent
flag.
JDK Mission Control's MBean Browser gc() operation on the
Memory bean is an explicit GC, with the diagnostic command
of GC.run again, like jcmd, is a diagnostic GC.
Another curiosity is that the jcmd
diagnostic
tool has changes over the years. In Java 7, in the GC logs,
I could not spot a difference in the GC Cause between the
explicit and diagnostic GC (enabled with
-XX:+PrintGCCause
). However, when invoking
GC.run
with jcmd
, the GC was
called. In Java 8, again the logs did not appear to show the
difference, but jcmd
prevented me from calling
the GC.run
diagnostic command when explicit GC
was disabled. From Java 11 onward, we see a clear distinction
in the GC logs between the explicit and diagnostic GC. In
addition, jcmd
allows us to issue the
GC.run
command even if we have disabled explicit GC.
Kirk pointed out to me that since the DiagnosticCommand is
just an MBean, we can also call that through JConsole or
VisualVM, by using the com.sun.management.DiagnosticCommand
MBean. We can even do that programmatically, like so:
import javax.management.*; import java.lang.management.*; public class DiagnosticCommand { public static void gcRun() { try { var server = ManagementFactory.getPlatformMBeanServer(); var name = new ObjectName( "com.sun.management:type=DiagnosticCommand"); server.invoke( name, "gcRun", new Object[0], new String[0]); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new IllegalStateException(e); } } }
Both the explicit and the diagnostic GC call
heap()->collect()
in the C++ layer of the JVM.
The diagnostic GC looks like this (diagnosticCommand.cpp)
Universe::heap()->collect(GCCause::_dcmd_gc_run);
The explicit GC is invoked with System.gc()
, and
is implemented like this (jvm.cpp)
if (!DisableExplicitGC) { EventSystemGC event; event.set_invokedConcurrent(ExplicitGCInvokesConcurrent); Universe::heap()->collect(GCCause::_java_lang_system_gc); event.commit(); }
A small tip. When using tools like jcmd
, instead
of using the process id (pid
), rather pass in the name of the
class. In other words, instead of jcmd 34512
, it is
often more convenient to call jcmd DiagnosticVsExplicitGC
. The only catch is
if we have several processes with the same name, then it will
send our diagnostic command to all of them. We might not want
this.
Let's stop the leaks and save our planet.
Kind regards
Heinz
We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.