Allocation and GC¶
Allocation is why GC is needed. Plus, allocation most directly reflects current allocation state, so allocation and GC are tightly coupled. This section covers four key points.
Trigger GC¶
alloc checks whether to trigger new GC cycle when allocating. Core code is very simple:
shouldhelpgc flag is highly correlated with allocation activity. If local cache hits (tiny object hit / small object cache hit / large object doesn't need refill), small objects only check when refilling from mcentral. Large objects check every time. "Check" here means "allocation is large so check if we've exceeded trigger", need to check again if we've actually exceeded trigger point.
TODO: should review allocation logic, this is quite important
Then call t.test() to check if current heap usage has reached GC trigger condition. If yes, start GC.
Mark Assist¶
Previously mentioned GC pacer controls assistRatio algorithm to ensure GC mark and alloc speeds stay consistent.
alloc is responsible for updating GC pacer data (heap usage, allocation speed, etc.). Plus, alloc gets assistRatio and other data from GC pacer to conditionally trigger assist mark.
Note that GC pacer allocates at least 25% CPU utilization for background GC workers. If it ends early, it won't use that much CPU.
When allocating memory, directly deduct current allocation size from credit.
If G owes debt (negative), must assist based on assistRatio via gcAssistAlloc.
assist happens before actual allocation, no new allocation happens until assist completes—meaning program heap usage doesn't increase (Strict Backpressure).
Assist quota is obtained from GC controller multiplied by allocated bytes, with a smoothing operation ensuring small allocations also scan some amount, accumulating some credit for next round without actual scanning.
Mutator Assist only happens when background GC power (that 25%) is insufficient to sustain current allocation rate. If background has surplus, Mutator prioritizes Credit Stealing, avoiding long tail latency.
Actual gc assist still calls gcDrain-like method to mark, consistent with previous logic.
Publication Barrier¶
(Concurrency safety)
Memory allocation uses publicationBarrier to ensure allocation becoming type-safe memory on heap is not discovered by GC.
This solves a problem: if CPU reorders instructions, and GC reads garbage data before memory allocation initializes, it causes memory corruption.
Note that newly allocated memory is all black—no need to worry about GC releasing newly allocated but unreferenced objects.
Barrier's purpose: ensure memory allocation and pointer-related operations fully complete before exposing to GC.
Large Objects: Delayed Zeroing¶
When allocating large objects, besides shouldhelpgc being set to true, delayedZeroing is also set to true.
When delayedZeroing is set, it zeroes in chunks (256KB),禁止抢占 while zeroing each chunk, but preemption allowed between chunks. 256KB comes from Go benchmarking, selected from some data.
Need to explain why memory zeroing needs preemption disabled: because memclr is written in assembly, contains no scheduler code inside, so once started scheduler has no way to interrupt at scheduler level. Can't do anything, whereas previous malloc is logically must-preempt—each P has its own cache, ensuring only current G can access P's memory, while allowing preemption means multiple Gs might access same P's memory.
Chunked zeroing solves large malloc causing long-tail STW. If zeroing one large memory chunk (2GB) at once, preemption is disabled during this process, one P is stuck, all other Ps wait for that P to finish. Since STW requires all Ps to stop, results in extra-long STW.
Essence: move long operations out of critical sections by making them interruptible.