Advanced Garbage Collection TODO
Concept
Modern GC avoids "stop and scan everything" by breaking the work into small pieces (incremental) or overlapping it with the application threads (concurrent). Concurrent marking creates the problem that the object graph changes while marking is in progress, so a write barrier records changes to maintain the tri-color invariant (a black object never points directly to a white object). Region-based GC divides the heap into uniformly sized regions and only processes the regions with high reclamation efficiency, decoupling total heap size from pause time. ZGC uses colored pointers and load barriers to perform even relocation (compaction) concurrently, while Go's GC is a non-moving, non-generational concurrent mark-sweep collector that paces its marking rate to hit a GOGC target. The common cost is throughput and memory usage — the more you cut pause time, the more barrier overhead and spare heap you need.
On a latency-sensitive server, GC is a factor that can wreck p99 all by itself no matter how fast the code is, and not understanding what the tuning knobs mean traps you in the blunt remedy of just growing the heap.
Code & Formula
# GC 심화 — 삼색(white/gray/black) 증분 마크 앤 스윕을 구현하고, 쓰기 배리어로 삼색 불변식을 지킨다.
class Obj:
def __init__(self, name):
self.name = name
self.refs = []
self.color = "white"
class IncrementalGC:
def __init__(self, roots):
self.gray = list(roots)
for r in roots:
r.color = "gray"
def write_barrier(self, holder, new_ref):
# 검은 객체가 흰 객체를 새로 가리키면, 흰 객체를 회색으로 되돌려 재방문시킨다
holder.refs.append(new_ref)
if holder.color == "black" and new_ref.color == "white":
new_ref.color = "gray"
self.gray.append(new_ref)
def mark_step(self, budget=1):
# 한 번에 budget개만 처리 — "멈추고 전부"가 아니라 조금씩 진행(증분 마킹)
steps = 0
while self.gray and steps < budget:
obj = self.gray.pop()
for r in obj.refs:
if r.color == "white":
r.color = "gray"
self.gray.append(r)
obj.color = "black"
steps += 1
def sweep(self, all_objs):
return [o for o in all_objs if o.color != "white"] # white == 도달 불가 -> 회수 대상
a, b, c, d = Obj("a"), Obj("b"), Obj("c"), Obj("d")
a.refs = [b]
b.refs = [c]
all_objs = [a, b, c, d] # d는 아무도 참조하지 않는 쓰레기
gc = IncrementalGC(roots=[a])
while gc.gray:
gc.mark_step(budget=1) # 애플리케이션과 번갈아 실행된다고 가정 (증분 마킹 흉내)
# 마킹이 끝난 뒤, 이미 black인 a가 새 객체 e를 가리키게 되는 상황을 시뮬레이션
e = Obj("e")
all_objs.append(e)
gc.write_barrier(a, e) # 쓰기 배리어가 e를 gray로 되돌려 재방문 대상에 넣어야 함
while gc.gray:
gc.mark_step(budget=1)
survivors = gc.sweep(all_objs)
print("생존(black):", [o.name for o in survivors])
print("회수 대상(white, 도달 불가):", [o.name for o in all_objs if o.color == "white"])
assert d.color == "white" and e.color == "black"
docs/code/algorithms/algorithms-29.py
Exercise
Turn on GODEBUG=gctrace=1 for a Go service and vary GOGC and the memory limit while recording how the GC cycle count, mark-assist volume, and p99 latency change.
Practical Connection
In allocation-heavy loops like block processing or order matching, reusing objects and cutting allocations pays off more than GC tuning does, and making that call requires understanding what the GC actually treats as cost.
If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.