가비지 컬렉션 상세 — 증분·세대 모드, 약한 테이블, 최종화

가비지 컬렉션 상세 — 증분·세대 모드, 약한 테이블, 최종화 (Garbage Collection in Depth)

가비지 컬렉션의 기본 개념은 "[가비지 컬렉션 (Garbage Collection)]" 절에서 다뤘어요. 여기서는 5.4의 세부 동작을 더 자세히 설명해요.

표시-수집 (Mark-and-Sweep) 기반

루아는 표시(mark)와 수집(sweep)을 수행하는 트레이싱(tracing) 가비지 컬렉터예요. 루트(root) — 실행 스택, 레지스트리, 전역, 업밸류, 그리고 C 스택 — 에서 시작해 도달 가능한(reachable) 모든 객체를 표시해요. 표시되지 않은 객체는 가비지로 판단돼 메모리를 회수해요. 좀비(zombie)나 프리(free) 상태를 넘어, 최종화가 필요한 객체는 별도로 처리해요.

세대별 보호 (Generational protection)

세대(generational) 모드에서 "늙은(old)" 객체가 "어린(young)" 객체를 가리키는 참조는 별도로 추적해요. 루아는 이렇게 오래된 객체에서 새 객체로의 참조를 알아야 어린 세대만 스캔해도 안전해요. 이 참조 목록(카디널리티)의 크기가 커지면 메이저 컬렉션이 트리거돼요.

약한 테이블 (Weak Tables)

약한 테이블의 키·값은 "약한 참조"로 저장돼요. 컬렉터가 약한 참조의 대상을 회수하면, 그 항목은 테이블에서 제거돼요. 약한 키나 약한 값은 그 객체가 다른 강한 참조로부터 도달 가능하지 않은 한 유지되지 않아요.

-- 값이 약한 캐시
local cache = setmetatable({}, { __mode = "v" })

약한 테이블의 제약:

  • 약한 키/값은 "에퍼머럴(ephemeron)"처럼 동작해, 자기 자신이나 상호 순환 참조가 있어도 수집될 수 있어요.
  • __mode"k"(키 약함), "v"(값 약함), "kv"(둘 다)로 설정해요.
  • 밸류가 약한 테이블에서 객체를 값으로 저장해도, 키가 강하면 수집되지 않을 수 있어요.

최종화 (Finalization)

__gc 메타메서드를 가진 객체(finalizable)가 도달 불가능해지면, 첫 컬렉션에서 최종화(파이널라이즈)되고, 이후 컬렉션에서 실제 메모리가 해제돼요. __gc는 한 번만 호출되고, 호출되는 순서는 보장되지 않아요. 최종화된 객체가 다시 살아나면(reached again) 더 이상 최종화 대상이 아니에요.

local t = setmetatable({}, {
  __gc = function() print("정리됨") end
})
t = nil
collectgarbage()   -- "정리됨" 출력 (시점은 결정적이지 않음)

컬렉터 파라미터

증분 모드에서:

  • pause: 컬렉터가 이전 주기 후, 메모리가 이 비율만큼 증가하면 새 주기를 시작해요. (백분율, 기본 200)
  • stepmul: 할당된 메모리 양에 대한 스텝 크기의 승수. (기본 100)
  • stepsize: 각 스텝의 세분화 정도.

세대 모드에서:

  • minor: 어린 세대 컬렉션의 빈도(보통 20, 낮을수록 자주).
  • major: 메이저 컬렉션의 빈도(보통 100).

collectgarbage("incremental", pause, stepmul, stepsize) 또는 collectgarbage("generational", minor, major)로 설정해요.

collectgarbage 옵션

  • "count" — 사용 중 메모리(KB) 반환.
  • "collect" — 전체 컬렉션.
  • "stop", "restart", "isrunning" — 실행 제어.
  • "step" — 한 단계 진행.
  • "setpause", "setstepmul" — 파라미터 설정.
  • "incremental", "generational" — 모드 전환.

메모리 관리 모범 사례

  • 수명이 짧은 객체가 많은 프로그램은 세대 모드가 유리할 수 있어요.
  • 캐시·모이제이션에는 약한 테이블을 사용하세요 — 객체가 사라지면 캐시도 자동 정리돼요.
  • __gc로 파일·소켓 같은 외부 자원을 정리할 수 있지만, to-be-closed 변수(<close>)가 더 결정적이고 예측 가능해요.
  • 파이널라이저는 실행 순서가 보장되지 않으므로, 다른 객체와의 상호작용에 의존하면 안 돼요.

출처: 가비지 컬렉션 상세 (Garbage Collection in Depth)

본문

컬렉터 동작 시점

값을 얼마든지 만들어도, 컬렉터는 스스로 결정한 시점(파라미터에 따라)에만 실행돼요. 프로그램이 collectgarbage("collect")를 호출하면 즉시 전체 컬렉션이 수행돼요. 메모리 부족으로 할당이 실패하면 루아는 그 시점에 컬렉터를 실행해 메모리를 확보하려 해요.

약한 참조와 유연한 설계

약한 테이블을 활용하면 객체의 수명과 "보조 데이터"의 수명을 자동으로 묶을 수 있어요. 전형적인 예는 객체 → 계산된 결과 캐시, 함수 → 메타데이터 매핑이에요.

더 알아보기