Articles
Java GC Interview Questions: Read Logs, Diagnose Pauses, and Choose What to Measure
Prepare for Java GC interviews by reading G1 logs, separating allocation from retention, diagnosing pause causes, and choosing useful performance measurements.
A Java service misses its latency target, and the dashboard shows frequent garbage collections. Should you enlarge the heap, change collectors, or search for a leak? A strong Java GC interview answer starts by separating those hypotheses. Collection frequency, pause duration, retained objects, and process memory describe different problems.
The useful skill is turning a log observation into a measurement that could prove your explanation wrong. This guide works through a G1 log, a misleading heap trend, and a collector experiment you can defend without promising a universal tuning recipe.
Evidence boundary: Official behavior below is grounded in Java SE 26 documentation. The numerical scenarios are original, synthetic interview exercises. A logging command was smoke-tested with local OpenJDK 26.0.2.1 and G1; it is not a production benchmark. No candidate reports or employer question-frequency claims are used.
What should you establish before interpreting a GC log?
Name the JDK build, collector, effective heap settings, CPU budget, and workload. Record whether the process runs inside a container. Ask whether the symptom is request latency, reduced throughput, memory growth, or termination. “GC is slow” is not a sufficiently precise problem statement.
Official behavior: G1 divides the heap into regions and combines concurrent work with stop-the-world collection pauses. Young collections, mixed collections that also reclaim selected old regions, and Full GC represent different work. Its pause target is a goal, not a hard deadline. These distinctions follow the Java 26 G1 guide.
For the exercises here, assume G1 and a fixed 1 GiB Java heap. This is an exercise constraint, not a recommendation for production. A log from another collector must be interpreted using that collector’s event definitions.
Also establish the time window. Startup compilation, cache warming, a traffic burst, and steady operation can produce different allocation patterns. Comparing a cold process with a warmed process makes a proposed improvement difficult to attribute.
A useful opening answer is: “I will align GC events with the affected requests, then distinguish the amount of work from the resources available to perform it.” That gives the interviewer a concrete investigation order.
How do you read a G1 pause line without overclaiming?
Consider this synthetic G1-style summary, with the uptime timestamp at the end of the event:
[12.000s][info][gc] GC(7) Pause Young (Normal) (G1 Evacuation Pause) 820M->240M(1024M) 18.000ms
Read it in layers. GC(7) identifies the collection for joining related log lines. Pause Young identifies the event type. The heap used before and after the event is approximately 820 MiB and 240 MiB; the parenthesized 1024 MiB is the heap capacity reported for this event. It is not process RSS, nor a universal statement about the configured maximum.
The arithmetic is simple: reported occupancy fell by 580 MiB, approximately 70.7% of the starting occupancy. That does not establish that 580 MiB of operating-system memory was returned. It also does not reveal allocation bytes per request.
The pause lasted 18 ms. It does not mean every request lasted 18 ms, and it does not prove that GC caused the slowest request. A request may already have been waiting for a database connection; requests arriving during the pause may accumulate behind other work afterward.
Original follow-up: Suppose another young pause completes at 14 seconds, with 800M->245M and a 20 ms duration. Under the simplifying assumption that no intervening event reclaimed heap space, occupancy rose about 560 MiB between the first pause’s end and the next pause’s start. The interval is 1.980 seconds, giving roughly 283 MiB/s of net growth.
Call it an estimate with explicit assumptions. Rounded occupancy values, omitted events, and collector activity can invalidate an allocation-rate calculation. Prefer an allocation profiler or recording when the question is which code paths allocate most.
Does a rising post-GC heap prove a memory leak?
No. Start by asking what was collected. A young-only collection is not a complete census of reclaimable objects throughout the heap. Comparing arbitrary post-GC points can confuse changing collection scope with changing retention.
Here is a second synthetic investigation. Three comparable observation windows, after old-generation reclamation and under similar load, show heap floors of 300, 450, and 600 MiB. That is stronger evidence of retained growth than three unrelated young-GC endpoints. It still does not establish unintended retention.
Three explanations deserve different tests:
| Possible explanation | Next evidence |
|---|---|
| Cache warming toward a limit | Entry count and configured cap |
| More requests in flight | Queue depth and concurrency |
| Objects retained after use | Retaining paths and owners |
For example, an unbounded map can retain completed request payloads because a long-lived service object still references the map. GC cannot infer that the application no longer needs those reachable objects. A bounded cache growing toward its intended capacity can produce a similar early trend without being a leak.
The distinguishing question is ownership: which reference keeps the object reachable, and should that reference still exist? Compare object populations over a representative interval and inspect paths from GC roots. A large object count alone does not answer whether the objects are useful.
A better interview conclusion is: “The growth supports a retention hypothesis. I would check whether it plateaus with cache size or active work before calling it a leak.” This preserves a useful diagnosis without outrunning the evidence.
What if RSS grows while the Java heap looks stable?
Treat resident set size (RSS), the process’s resident memory, as a separate measurement. Java heap occupancy does not include every memory consumer in the process. Thread stacks, class metadata, code, direct buffers, and native components can matter even when the heap floor is stable.
Official tooling boundary: Oracle’s diagnostic tools documentation describes Native Memory Tracking and its limits. NMT is useful for tracked HotSpot categories; it is not a complete accounting of arbitrary third-party native allocations. Reserved, committed, and resident memory are also different measurements.
Synthetic case: A container has a 2 GiB memory limit and -Xmx1500m. The process is killed while its Java heap occupies only 900 MiB. “The heap was below Xmx, so memory cannot be the problem” is an invalid conclusion. Heap maximum is not a cap on all memory charged to the container.
Inspect the termination reason, container memory usage, thread counts, direct-buffer activity, and native-memory evidence. Do not label an operating-system kill as a Java heap OutOfMemoryError without the corresponding evidence. Enlarging the Java heap could reduce the remaining process-memory headroom.
Which logs and recordings would you collect first?
Use a bounded capture that answers a stated question. Here is a Java 26 logging example; replace the jar name with the application under investigation:
java -XX:+UseG1GC \
'-Xlog:gc*,safepoint:file=gc.log:time,uptime,level,tags:filecount=5,filesize=20M' \
-jar service.jar
Official syntax: The Java command reference defines unified logging selectors, decorators, and file rotation. gc* includes tag sets containing gc; gc alone matches the exact tag set. Wall-clock time helps align service telemetry, while uptime identifies events within one process lifetime.
The command’s logging selector and rotation options were accepted in the local G1 smoke test. That verifies syntax and log production, not operational overhead or suitability for a particular deployment. The test’s tiny heap and allocation loop are deliberately not offered as tuning guidance.
For an attached process, first discover the commands supported by that runtime:
jcmd <pid> help
jcmd <pid> VM.flags
jcmd <pid> GC.heap_info
jcmd <pid> JFR.start name=gc-investigation settings=profile duration=60s filename=gc-investigation.jfr
Official tool behavior: The jcmd reference documents these commands and their impact. A heap dump can be expensive and ordinarily requests a full GC unless the relevant option changes that behavior. A histogram, recording, and heap dump answer different questions; do not collect all three reflexively during a latency incident.
Use a recording to connect allocation sites, CPU activity, and pauses over time. For retained growth, a heap dump may provide the object graph that a log cannot. Evaluate capture overhead and handle any captured application data appropriately.
Can changing the pause target fix request p99?
Not by itself. A target such as -XX:MaxGCPauseMillis=50 is neither a request-latency SLO nor a guarantee that every pause stays below 50 ms.
Official tuning guidance: Oracle recommends beginning G1 tuning with its defaults and a considered heap size and pause goal. Fixing the young-generation size can undermine G1’s ability to adapt its pause work. The G1 tuning guide also describes checking phase timings and CPU-time information rather than treating every long pause as the same failure.
Original metric exercise: Over a complete 60-second window, twenty 10 ms pauses contribute 200 ms, or about 0.33% of wall-clock time. Add one 400 ms pause and the total becomes 600 ms, or 1%. The average across the 21 pauses is about 28.6 ms, which hides the 400 ms event.
Neither average tells you request p99. You need request measurements, their distribution over time, arrival rate, and queueing behavior. A small fraction of total time paused can still damage a burst-sensitive service.
Also avoid adding concurrent-cycle duration to stop-the-world pause totals. Concurrent work can compete for CPU, but it is not equivalent to stopping all application threads for that entire interval. Explain each measurement’s denominator before presenting a percentage.
How would you compare collectors or heap sizes fairly?
First state the objective: successful requests per second at a latency target, batch completion time, or memory cost. Official collector guidance distinguishes throughput-oriented choices from collectors that prioritize short pauses; the available collectors documentation does not supply a universal winner for every workload.
Design a controlled comparison with the same JDK build, traffic shape, dataset, warmup policy, and CPU/container limits. Change one dimension initially. Record effective heap settings because a collector switch can change runtime behavior beyond the flag’s name.
Measure request p50/p95/p99, errors, throughput, pause distribution, CPU, and memory headroom over repeated representative runs. Include overload behavior. If the load generator waits for each response before sending the next request, a stalled service can reduce offered load and conceal the intended demand pattern.
Synthetic decision: Configuration A achieves 8,000 successful requests/s at 90 ms p99; B reports 60 ms p99 at 5,000 requests/s. Without matched offered load, these numbers do not establish that B improved the same service objective. Lower latency may reflect doing less work.
State the acceptance criterion before the experiment, such as meeting the existing latency SLO at the required traffic with sufficient memory headroom. Then explain how you would roll back if the change increases errors or destabilizes peak traffic. This is a stronger answer than naming a favorite collector.