GC tuning versus manual management
How does headroom set collector cost, and where is the ceiling of tuning?
Suppose you have not gone off-heap. What does tuning buy, and where is its ceiling?
The one load-bearing relationship
Collector CPU overhead falls roughly as
- the heap size
- the live set
Data table
| Heap ÷ live set H/L | Overhead (relative) |
|---|---|
| 1.05 | 20× |
| 1.16 | 6.18× |
| 1.27 | 3.65× |
| 1.39 | 2.59× |
| 1.5 | 2.01× |
| 1.61 | 1.64× |
| 1.72 | 1.39× |
| 1.83 | 1.2× |
| 1.94 | 1.06× |
| 2.06 | 0.946× |
| 2.17 | 0.856× |
| 2.28 | 0.781× |
| 2.39 | 0.718× |
| 2.5 | 0.665× |
| 2.62 | 0.619× |
| 2.73 | 0.579× |
| 2.84 | 0.543× |
| 2.95 | 0.512× |
| 3.06 | 0.485× |
| 3.18 | 0.46× |
| 3.29 | 0.437× |
| 3.4 | 0.417× |
| 3.51 | 0.398× |
| 3.62 | 0.381× |
| 3.73 | 0.366× |
| 3.85 | 0.351× |
| 3.96 | 0.338× |
| 4.07 | 0.326× |
| 4.18 | 0.314× |
| 4.29 | 0.304× |
| 4.41 | 0.294× |
| 4.52 | 0.284× |
| 4.63 | 0.275× |
| 4.74 | 0.267× |
| 4.85 | 0.259× |
| 4.97 | 0.252× |
| 5.08 | 0.245× |
| 5.19 | 0.239× |
| 5.3 | 0.232× |
| 5.41 | 0.227× |
| 5.52 | 0.221× |
| 5.64 | 0.216× |
| 5.75 | 0.211× |
| 5.86 | 0.206× |
| 5.97 | 0.201× |
| 6.08 | 0.197× |
| 6.2 | 0.192× |
| 6.31 | 0.188× |
| 6.42 | 0.185× |
| 6.53 | 0.181× |
| 6.64 | 0.177× |
| 6.76 | 0.174× |
| 6.87 | 0.17× |
| 6.98 | 0.167× |
| 7.09 | 0.164× |
| 7.2 | 0.161× |
| 7.31 | 0.158× |
| 7.43 | 0.156× |
| 7.54 | 0.153× |
| 7.65 | 0.15× |
| 7.76 | 0.148× |
| 7.87 | 0.145× |
| 7.99 | 0.143× |
| 8.1 | 0.141× |
| 8.21 | 0.139× |
| 8.32 | 0.137× |
| 8.43 | 0.135× |
| 8.55 | 0.133× |
| 8.66 | 0.131× |
| 8.77 | 0.129× |
| 8.88 | 0.127× |
| 8.99 | 0.125× |
| 9.11 | 0.123× |
| 9.22 | 0.122× |
| 9.33 | 0.12× |
| 9.44 | 0.118× |
| 9.55 | 0.117× |
| 9.66 | 0.115× |
| 9.78 | 0.114× |
| 9.89 | 0.113× |
| 10 | 0.111× |
where H is the heap size and L is the live set. Headroom (H − L) is what you are buying.
(Strictly, it is doubling the headroom H − L, not the heap, that halves collection frequency. In the table above, going from 40 GB to 80 GB triples the headroom, from 20 GB to 60 GB, and cuts the relative overhead from 1 to 0.33.)
This one formula explains why both "heap too small" and "heap too large" produce pauses, by different routes:
- Too small: H/L → 1, so collection frequency diverges.
- Too large: each traversal takes longer and touches more cold memory, so pause duration and page-cache damage rise.
The knee is not a matter of taste. It is the shape of that curve.
The three knobs, described as what they are
- Heap size buys headroom, the denominator above.
- Young-generation sizing buys the death-before-promotion property. Premature promotion is the classic pathology: a hash table built through a young generation that is too small gets copied into the old generation, where collecting it is expensive. You paid for the copy and for the expensive collection, for an object that would otherwise have died for free.
- Collector choice is an explicit position on the latency/throughput trade-off. Throughput collectors maximize work per CPU-second. Low-pause collectors buy bounded pauses with concurrent relocation and program barriers, and spend throughput to do it.
The ceiling
No tuning changes the fact that the collector's cost is a function of the live set and that the collector cannot see your semantics. There are only two stable positions:
- When the live set is dominated by one structure that you can describe in bytes rather than in objects, that structure should leave the heap.
- When the live set is small and the problem is allocation churn, tune the young generation and stop.
For a Spark 2.4 / JDK 8 environment the low-pause escape (ZGC-class collectors) is not available. The usable levers are the choice among Parallel, CMS and G1, young-generation sizing, and Tungsten's off-heap mode. So the crossover to "take it off-heap" arrives at a smaller live set than on a modern runtime. That is a real constraint on the decision, not a footnote.
Contention
A collector is a contended resource in the 1/(1−ρ) sense, where ρ is the allocation rate divided by the collection capacity. Pause frequency, and the latency it causes, rises non-linearly as ρ approaches 1. It is the same curve as hash-table load factor, a simple queue, and scalability laws. (What happens past the knee, where pauses slow the program and the backlog grows, is the GC death spiral, which is a failure-mode topic. Here we only need the curve.)
One-line compression: GC cost ≈ live set ÷ headroom. Tuning moves you along that curve; off-heap changes which term you are standing on.