HotSpot's Concurrent Mark-Sweep collector had to begin its concurrent old-generation cycle well before the old generation was actually full. Why could it not simply wait until the space ran out, and what were the costs of starting too early or too late?
answer
- cycle must finish before allocation fills the old gen
- headroom ≈ promotion rate × cycle duration + margin
- CMSInitiatingOccupancyFraction + UseCMSInitiatingOccupancyOnly
- too late = concurrent mode failure; too early = wasted CPU
- floating garbage means live set is effectively larger
basics
~20 sIts marking and sweeping ran concurrently with the application, which keeps allocating and promoting the whole time. If the cycle starts when the old generation is already full, there is no space to serve those allocations before it finishes, so the JVM falls back to a long stop-the-world full collection. Starting too early wastes CPU on extra cycles.
solid answer
~60 sA concurrent collector reclaims space *while* the application keeps producing garbage and promoting survivors, so a cycle is a race: it must finish reclaiming before allocation exhausts what is left. If CMS waited until the old generation was full, there would be no headroom to cover the cycle's duration, and every cycle would end in a **concurrent mode failure** — the stop-the-world compacting full collection it existed to avoid. So it started at an **initiating occupancy** threshold. By default the collector estimated the point adaptively from recent cycle durations and promotion rate; `-XX:CMSInitiatingOccupancyFraction=<pct>` with `-XX:+UseCMSInitiatingOccupancyOnly` pinned it to a fixed percentage. Costs on both sides: - **Too late**: the race is lost — concurrent mode failure, a multi-second pause, and, if it recurs, a service that looks like it hangs periodically. - **Too early**: cycles run more often, so more CPU goes to marking and to write-barrier work, throughput drops, and the collector spends effort on a heap that was not yet under pressure. The threshold also had to absorb **floating garbage** — objects that die after being marked survive to the next cycle — so a concurrent collector always needs more heap than its live set.
code
text · 3 lines-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=70
-XX:+UseCMSInitiatingOccupancyOnlygo deeper
Say the collector works while the program keeps allocating, so it must start with space left over or it will run out mid-cycle.
Quantify the idea as headroom covering promotion rate times cycle duration, and name the two failure directions.
Discuss reading the GC log to decide the threshold, why failures cluster at peak, floating garbage in heap sizing, and that fragmentation is a separate problem tuning cannot fix.
Frame the collector as converting heap headroom and CPU into pause reduction, and drive capacity planning from the fallback's worst case under peak rather than from median pause times.
## The race a concurrent collector runs A stop-the-world collector has a simple contract: when space runs out, stop everyone, reclaim, resume. It can afford to wait until the last moment because nothing is allocating while it works. A concurrent collector cannot. Its marking and sweeping overlap with application execution, and during that window application threads keep allocating and keep promoting survivors out of the young generation into the old one. The cycle therefore has to be *started early enough that the space remaining when it starts covers everything allocated before it finishes*. That is the whole reason an initiating threshold exists. The required headroom is roughly the promotion rate multiplied by the cycle duration, plus a margin. Both inputs move with load: a traffic spike raises the promotion rate, and CPU contention lengthens the cycle, so the two errors compound exactly when the system is busiest. This is why concurrent-collector incidents cluster at peak. ## How the threshold was chosen By default CMS ran an adaptive heuristic, estimating from recent history how long a cycle takes and how fast the old generation grows, and triggering when the projection said it should. Operators frequently overrode it because the estimate reacted too slowly to bursty workloads: - `-XX:CMSInitiatingOccupancyFraction=<percent>` set the old-generation occupancy at which a cycle begins; - `-XX:+UseCMSInitiatingOccupancyOnly` told the JVM to use that number every time instead of only as the first cycle's seed. Tuning meant watching the GC log: if concurrent-mode failures appeared, lower the fraction (start earlier) or add heap; if the collector was cycling constantly on a heap that never came close to filling, raise it. ## Floating garbage and why headroom is structural Marking concurrently means marking a moving target. An object that is reachable when the marker visits it, and becomes garbage a millisecond later, is still marked live for this cycle — the collector cannot un-mark it safely once the marking wavefront has passed. That object is **floating garbage**: real garbage that survives until the next cycle collects it. The practical consequence is that a concurrent collector's effective live set is always larger than the application's true live set, by an amount proportional to allocation rate and cycle duration. Heap sizing must include it. A heap sized to the true live set plus a thin margin will start failing cycles under load even with a perfectly tuned threshold, because floating garbage eats the margin. ## The two failure directions **Starting too late** is the dangerous one. The old generation fills mid-cycle, the concurrent attempt is abandoned, and the JVM performs a stop-the-world mark-sweep-compact full collection — in older JDKs single-threaded, so its duration scales with the entire live set. A service with 30 ms pauses suddenly shows a multi-second freeze. Because the trigger is load-correlated, one failure often begets more: the long pause backs up queues, the backlog raises allocation rate, and the next cycle is even more likely to lose its race. **Starting too early** is merely wasteful, but the waste is real. Each cycle costs CPU for the marking threads and produces two stop-the-world phases of its own. On a machine where the application already uses most of the cores, that CPU comes directly out of application throughput. Continuous cycling — sometimes visible as back-to-back cycles in the log with no idle gap — means the collector is consuming a constant slice of the machine to keep pace. ## Fragmentation interacts with the timing question Even a perfectly timed cycle can be defeated by the collector's other weakness. Because CMS swept in place and never compacted, the old generation's free space fragmented over time, so a promotion could fail for lack of a *contiguous* chunk at an occupancy level the threshold considered comfortable. Timing tuning cannot fix that; only compaction can, and CMS compacted only in the fallback full collection. ## The generalisable lesson Every concurrent collector after CMS inherits this same structure — pick a trigger point, keep enough headroom to cover a cycle, and fail over to something drastic if the race is lost. The parameters differ and the modern collectors do far more of it adaptively, but the reasoning transfers: **a concurrent collector converts heap headroom and CPU into pause-time reduction**. If you refuse to pay in either currency, you get the long pause back at the worst possible moment. When capacity-planning a latency-sensitive JVM service, the question is not "what is the average pause" but "how much headroom does the collector need to keep the fallback from ever firing under peak load".
- Why does a concurrent collector need more heap than a stop-the-world one for the same application?Two reasons. It must reserve headroom to absorb everything allocated and promoted while a cycle runs, because the application never stops. And it accumulates floating garbage — objects that die after the marker has already marked them live, which are only reclaimed by the following cycle. Both effects inflate the effective live set, so sizing the heap to the true live set plus a thin margin guarantees failed cycles under load.
- Concurrent-mode failures appear only during traffic peaks. What makes peak load the trigger?Peak load raises the promotion rate, so the old generation fills faster after the cycle starts, and it also increases CPU contention, which lengthens the cycle itself. Required headroom rises while available time shrinks, so the race is lost precisely when the system is busiest. The failure is also self-reinforcing: the long pause creates a backlog that raises allocation rate for the next cycle.
saying these in an interview costs you the question
- Saying the collector should just start when the old generation is full, as a stop-the-world collector does
- Treating the initiating threshold as a free knob with no CPU cost when lowered
- Ignoring floating garbage when sizing the heap for a concurrent collector
- Believing threshold tuning can also solve fragmentation-driven promotion failures
- Concluding from a good median pause that a concurrent collector is safely configured