Workspace IndexAlgorithms › Day 29

Advanced Garbage Collection TODO

Algorithms · Day 29 / 100 · B. Compilers, Runtimes & VMs (Day 20-35)

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"

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/.


한국어

GC 심화 TODO

Algorithms · Day 29 / 100 · B. 컴파일러·런타임·VM (Day 20–35)

증분·병행·리전 기반(ZGC·Go)

개념

현대 GC는 "멈추고 전부 훑기"를 피하기 위해 작업을 잘게 쪼개거나(증분) 애플리케이션 스레드와 겹쳐서(병행) 수행한다. 병행 마킹은 마킹 중에 객체 그래프가 변하는 문제를 낳으므로, 쓰기 배리어로 변경을 기록해 삼색 불변식(검은 객체가 흰 객체를 직접 가리키지 않는다)을 유지한다. 리전 기반 GC는 힙을 균일한 크기의 리전으로 나누고 회수 효율이 높은 리전만 골라 처리해, 전체 힙 크기와 정지 시간의 결합을 끊는다. ZGC는 컬러드 포인터와 로드 배리어를 써서 재배치(compaction)까지 병행으로 수행하고, Go의 GC는 비이동·비세대형 병행 마크스윕이며 GOGC 목표에 맞춰 마킹 속도를 조절하는 페이싱을 쓴다. 공통 대가는 처리량과 메모리 사용량으로, 정지 시간을 줄인 만큼 배리어 오버헤드와 여유 힙이 필요하다.

지연시간이 중요한 서버에서 GC는 코드가 아무리 빨라도 p99를 혼자 망칠 수 있는 요인이고, 튜닝 손잡이의 의미를 모르면 힙만 키우는 대증요법에 갇힌다.

코드 · 수식

# 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"

연습

Go 서비스 하나에 GODEBUG=gctrace=1을 켜고 GOGC와 메모리 리밋을 바꿔가며 GC 주기·마킹 보조(assist) 발생량·p99 지연 변화를 기록해 보기.

실무 · Verex 연결

블록 처리나 주문 매칭처럼 할당이 몰리는 루프에서는 객체 재사용과 할당 줄이기가 GC 튜닝보다 효과가 크고, 그 판단을 하려면 GC가 무엇을 비용으로 삼는지 알아야 한다.

공부한 날 원본 커리큘럼(docs/knowledge/dev-100-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 28. WASM 실행 모델과 샌드박싱 경계30. Rust 소유권·차용 검사기 내부(NLL)와 우회 패턴 →