Garbage collection recovers storage from objects the runtime can no longer reach; it does not close application resources or guarantee a fixed pause time.
Java garbage collection: diagnose retention before tuning
Java 21+. Use a JDK that supports this release.
Compare the same workload phases
Capture a baseline after startup, run a bounded workload, allow it to reach its usual idle state, then compare the retained trend. One high heap reading during an allocation burst is different evidence from a rising floor after repeated equivalent batches.
The program measures currently used heap through MemoryMXBean before and after allocating a small retained buffer. The numbers vary across runs and are not a deterministic leak test. Collection, heap resizing, and other runtime work can alter them.
A histogram identifies common classes; it does not prove why they remain reachable. A heap dump and ownership analysis can show the retaining path. Dumps may contain application data and need an intentional storage and access policy.
Choose a diagnostic before a collector flag
For a Java 21 run, unified GC logging can show collection events and pause information. A bounded log file keeps a long-running diagnostic from consuming an unbounded disk budget. Compare latency and throughput under the same input rather than assuming fewer collections means a faster service.
System.gc is a request, not a contractual test barrier. Do not insert it between production requests to compensate for an unbounded cache. Reduce the retaining owner or give it an explicit capacity first.
A collector cannot reclaim an object still reachable through a live owner. Reachability explains that requirement. Different collectors trade pause behavior, concurrent work, and memory headroom; no one setting removes those tradeoffs.
Working program
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
public class HeapObservation {
private static byte[] retained;
public static void main(String[] args) {
MemoryMXBean memory = ManagementFactory.getMemoryMXBean();
long before = memory.getHeapMemoryUsage().getUsed();
retained = new byte[256 * 1024];
long after = memory.getHeapMemoryUsage().getUsed();
System.out.println("beforeBytes=" + before);
System.out.println("afterBytes=" + after);
System.out.println("retainedBytes=" + retained.length);
retained = null;
}
}Cost and design choices
The buffer uses O(n) payload storage for n bytes. Clearing this one retaining reference permits later reclamation if no other owner exists; it does not synchronously lower the heap measurement.
Diagnostic capture adds work. A live heap dump or profiling session can perturb latency and may require a maintenance plan. Record the tool settings alongside the measured result.
Common Mistakes
- Do not label every temporary peak a memory leak.
- Do not use forced collection as proof of production behavior.
- Do not tune a collector before checking an unbounded retaining structure.
Connect the contracts
Compare the boundary explained in Enclosing references with the assumptions made by this program.
