🧠 Section 3 · Question #27

Can you manually invoke GC

System.gc() is strictly a non-binding suggestion (hint) to the virtual machine.


🟢 Junior Level

Yes, you can programmatically request a Garbage Collection cycle in Java by invoking:

System.gc();
// Or its direct equivalent:
Runtime.getRuntime().gc();

Does System.gc() Guarantee Garbage Collection?

No. According to the Java Language Specification (JLS §12.8) and standard JavaDoc:

“Calling the gc method suggests that the Java Virtual Machine expend effort toward recycling unused objects… There is no guarantee that this effort will recycle any particular object or immediately reclaim memory.”

System.gc() is strictly a non-binding suggestion (hint) to the virtual machine.

However, in standard HotSpot JVM builds with default configurations, invoking System.gc() forces an immediate, synchronous Stop-The-World Full Garbage Collection across all generations of the Java Heap, halting all application threads.

External Invocations via CLI

Garbage collection can also be triggered externally without modifying application source code:

# Trigger an immediate on-demand GC via the HotSpot Attach API:
jcmd <PID> GC.run

🟡 Middle Level

1. Invocations Channels in the Java Platform

Invocation Method Context Default HotSpot Behavior
System.gc() Standard Java application code Synchronous Stop-The-World Full GC
Runtime.getRuntime().gc() Underlying JVM native call Identical to System.gc() (invoked by it)
MemoryMXBean.gc() JMX Management Beans Delegates directly to Runtime.getRuntime().gc()
jcmd <PID> GC.run Host terminal CLI Dispatches VM_GC_Operation via Attach API

2. HotSpot Tuning Flags for Explicit GC

HotSpot allows platform engineers to override or modify how the runtime handles explicit GC requests:

# Option 1: Completely discard all explicit GC requests (converts them to no-ops)
java -XX:+DisableExplicitGC -jar application.jar

# Option 2: Transform explicit GC into a concurrent background cycle (Recommended)
java -XX:+ExplicitGCInvokedConcurrent -jar application.jar
  • -XX:+DisableExplicitGC: Completely ignores calls to System.gc(). The method returns immediately without executing any memory management work.
  • -XX:+ExplicitGCInvokedConcurrent: The production standard for concurrent collectors (G1, ZGC, Shenandoah). Instead of triggering a catastrophic Stop-The-World Full GC pause, the JVM executes a non-blocking concurrent marking and reclamation cycle in the background while application threads continue serving traffic.

3. Legitimate Use Cases for Manual Invocations

While calling System.gc() in business logic is an architectural anti-pattern, rare engineering scenarios warrant its use:

  1. Microbenchmarking (Java Microbenchmark Harness - JMH): JMH invokes System.gc() between warmup and measurement iterations to establish an uncontaminated heap baseline.
  2. Post-Mortem Diagnostics: Forcing a collection prior to generating a heap dump (jcmd <PID> GC.heap_dump -all=false) to eliminate ephemeral dead objects.
  3. Short-Lived CLI Batch Scripts: Triggering a cleanup after a massive, one-time data transformation phase (e.g., parsing a 10 GB XML file) before launching the next processing stage.

🔴 Senior Level

1. The -XX:+DisableExplicitGC Trap: Direct Memory (NIO) Failures

A pervasive operational error in production environments is setting -XX:+DisableExplicitGC to block third-party libraries from invoking System.gc(). In network-intensive applications using Netty, gRPC, or Kafka, this flag can cause fatal crashes:

[NIO Thread: ByteBuffer.allocateDirect()] ──► [Off-Heap Memory Saturated]
                        │
                        ▼
       [jdk.internal.misc.Bits.reserveMemory()]
                        │
                        ▼
          [Forces System.gc() Cleanup]
                        │
       ┌────────────────┴────────────────┐
       ▼                                 ▼
[-XX:+DisableExplicitGC Enabled]  [-XX:+ExplicitGCInvokedConcurrent Enabled]
       │                                 │
       ▼                                 ▼
[Call Ignored! Cleaners NOT run]  [Concurrent GC cleans PhantomReferences]
       │                                 │
       ▼                                 ▼
[OutOfMemoryError: Direct buffer] [Native memory freed successfully]
(Heap is 90% FREE!)

Why Direct Buffers Depend on System.gc():

  1. Off-heap memory allocated via ByteBuffer.allocateDirect() allocates native memory via C malloc().
  2. The on-heap Java wrapper (DirectByteBuffer) is tiny ($\sim 64\text{ bytes}$). The JVM Heap experiences virtually zero memory pressure, so the GC engine never schedules an autonomous collection.
  3. Native memory deallocation relies on java.lang.ref.Cleaner (a PhantomReference), which frees the off-heap address only after the 64-byte Java wrapper is garbage collected.
  4. When native off-heap memory is exhausted (-XX:MaxDirectMemorySize), the internal allocator (Bits.reserveMemory()) issues a last-resort call to System.gc() to collect orphaned wrappers and trigger cleaners.
  5. If -XX:+DisableExplicitGC is set, the emergency call is discarded, no cleaners run, and the JVM crashes with java.lang.OutOfMemoryError: Direct buffer memory.

[!TIP] Production Best Practice: Never use -XX:+DisableExplicitGC. Always configure -XX:+ExplicitGCInvokedConcurrent, which satisfies NIO direct buffer cleaner cycles while preventing disruptive Stop-The-World Full GC pauses.


2. Tracking Down Hidden System.gc() Calls in Production

When GC logs reveal periodic Full GC spikes caused by unvetted libraries:

# 1. Enable unified JVM GC logging to expose the trigger cause:
-Xlog:gc*,gc+phases=debug:file=/var/log/gc.log:time,uptime,pid:filecount=5,filesize=100M
# Look for: "Pause Full (System.gc())" or "Pause Young (System.gc())"

# 2. Pinpoint the exact calling class and line using Java Flight Recorder (JFR):
jcmd <PID> JFR.start name=DetectSystemGC settings=profile duration=10m filename=/tmp/diag.jfr

In JDK Mission Control (JMC), filter for the System GC or Garbage Collection event; JFR captures the exact thread name, timestamp, and full Java method call stack down to the precise source code line that invoked System.gc().


4 Tricky Questions

1. Does the Java Language Specification guarantee that garbage is collected after calling System.gc()?

Answer: No. The Java Language Specification (JLS §12.8) and JVM specifications define System.gc() strictly as a non-binding recommendation (hint). The specification explicitly provides the JVM vendor full latitude to postpone the collection, perform a minimal young-generation sweep, execute a concurrent background pass, or ignore the invocation entirely.

While HotSpot defaults to triggering a synchronous Full GC when unconfigured, the call remains non-deterministic by contract, and production code must never rely on it for operational correctness.


2. Why can configuring -XX:+DisableExplicitGC trigger fatal OutOfMemoryError: Direct buffer memory crashes in Netty or gRPC services?

Answer: Network frameworks allocate off-heap memory via ByteBuffer.allocateDirect(), which allocates physical memory outside the Java heap. Because the corresponding Java DirectByteBuffer objects on the heap are small ($64\text{ bytes}$), the Java heap can remain 90% empty even when gigabytes of off-heap memory are in use. Consequently, the JVM’s autonomous heap-pressure heuristics never trigger a garbage collection.

Deallocation of this off-heap native memory relies on Cleaner objects (subclasses of PhantomReference). When direct memory allocation reaches -XX:MaxDirectMemorySize, the JDK’s internal Bits.reserveMemory() method explicitly calls System.gc() in a sleep-retry loop to trigger phantom-reference processing and free native memory.

If -XX:+DisableExplicitGC is set, this emergency request is discarded as a no-op. The cleaners are never executed, off-heap memory is never released, and the service crashes immediately with java.lang.OutOfMemoryError: Direct buffer memory.


3. What does the flag -XX:+ExplicitGCInvokedConcurrent do under G1, ZGC, or Shenandoah, and why is it superior to disabling explicit GC?

Answer: Under modern concurrent collectors (G1, ZGC, Shenandoah), calling System.gc() by default forces a single-threaded or unoptimized multi-threaded Stop-The-World Full GC, halting application mutators for seconds.

Setting -XX:+ExplicitGCInvokedConcurrent instructs the JVM to translate explicit GC calls into a concurrent cycle:

  • In G1 GC, it triggers a Concurrent Mark cycle (g1ConcurrentMark.cpp), marking and reclaiming old regions concurrently without a Full GC STW pause.
  • In ZGC and Shenandoah, it initiates standard concurrent marking and relocation phases.
  • It is superior to -XX:+DisableExplicitGC because it satisfies direct buffer deallocation and RMI Distributed Garbage Collection requirements without freezing mutator threads.

4. How can you identify the exact class and line of code triggering periodic System.gc() calls in production without access to source code?

Answer:

  1. Java Flight Recorder (JFR): Start a lightweight JFR recording via jcmd <PID> JFR.start settings=profile duration=5m filename=gc_trace.jfr. Open the file in JDK Mission Control (JMC) and navigate to the Garbage Collections panel. Select the event with cause System.gc(). JMC displays the exact thread, timestamp, and full stack trace showing the calling class, method, and line number.
  2. Bytecode Instrumentation (BTrace / Arthas): Hot-attach Alibaba Arthas to the running process and execute:
    [arthas]$ trace java.lang.System gc
    

    Arthas intercepts the invocation in real time and prints the call stack to the terminal.


🎯 Interview Cheat Sheet

30-Second Summary

“You can manually request garbage collection via System.gc(), Runtime.getRuntime().gc(), or jcmd <PID> GC.run. According to the JLS, it is strictly a non-binding hint, but HotSpot defaults to executing a disruptive Stop-The-World Full GC. In production, manual invocations in application code are an anti-pattern. While -XX:+DisableExplicitGC completely disables explicit calls, it can cause native off-heap memory exhaustion in NIO/Netty applications. The recommended production standard is -XX:+ExplicitGCInvokedConcurrent, which converts explicit GC calls into non-blocking background cycles.”

Common Follow-up Questions

  • “What is RMI DGC and why does it call System.gc()?” → Java Remote Method Invocation uses Distributed Garbage Collection (DGC) to clean up remote object stubs, historically triggering periodic System.gc() calls every hour (controlled by sun.rmi.dgc.client.gcInterval).
  • “Can MemoryMXBean trigger a GC?” → Yes, ManagementFactory.getMemoryMXBean().gc() delegates directly to Runtime.getRuntime().gc().
  • “Does System.gc() guarantee the execution of finalizers?” → No, finalizer execution is managed by the Finalizer daemon thread; System.runFinalization() requests finalizer execution, but is also non-binding and deprecated.

Red Flags (DO NOT Say)

  • ❌ “Calling System.gc() guarantees that all unused memory is immediately freed.” (JLS defines it as a suggestion; the JVM is not required to reclaim anything).
  • ❌ “I always place System.gc() in finally blocks after database operations.” (Severe performance anti-pattern that creates latency spikes and disrupts GC heuristics).
  • ❌ “Disabling explicit GC with -XX:+DisableExplicitGC has zero side effects.” (Breaks off-heap direct buffer reclamation, causing Direct buffer memory OOM crashes).