가비지 컬렉션 (Garbage Collection)

"Automatic memory management, zero-manual-free"

— 개발자가 malloc과 free를 손수 타이핑하며 수동으로 메모리를 해제하던 실수의 시대를 끝내기 위해 주창된 패러다임.

분명 메모리를 '알아서' 비워준다고 해서 썼는데, 정작 잊을 만하면 찾아와 시스템을 통째로 몇 초 동안 일시 정지(Stop-the-world) 시키는 전산계의 기습 렉 유발자. 결국 메모리 관리를 신경 안 쓰려고 GC를 썼으나, 극도의 고성능 시스템에서는 이 GC 오버헤드를 피하려고 또 다른 튜닝 삽질을 하고 있다.(...)

1. 개요

가비지 컬렉션(Garbage Collection, GC)은 동적으로 할당된 메모리 영역 중 더 이상 사용되지 않는 영역(쓰레기, Garbage)을 탐지하여 자동으로 회수하는 메모리 관리 기법이다. C언어C++처럼 개발자가 수동으로 메모리를 지우지 않아도 되게끔, 자바 가상 머신(JVM)이나 자바스크립트V8 엔진 같은 런타임 환경이 뒤에서 보이지 않게 성실하게 동작한다.(...)

2. GC의 심장: 어떻게 쓰레기를 판별하는가?

무엇이 쓰레기인지 판단하는 알고리즘은 성능과 정합성의 저울질 속에서 끊임없이 진화해 왔다.

2.1. 참조 횟수 계산 (Reference Counting)

  • 객체를 가리키는 포인터(참조)의 개수를 세어둔다. 가리키는 놈이 0개가 되는 순간 메모리에서 지워버리는 매우 단순하고 직관적인 방식이다.
  • 하지만 영희와 철수가 서로를 가리키는 순환 참조(Circular Reference) 관계가 형성되면, 둘 다 정작 아무도 안 씀에도 참조 카운트가 결코 0이 되지 않아 영원히 메모리에 남아 누수(Leak)를 유발하는 치명적인 단점이 있다.

2.2. 마크 앤 스윕 (Mark and Sweep)

  • 현대 대다수 고성능 엔진이 채택한 방식이다.
  • 메모리의 출발점인 Root Set에서 출발하여 가지를 타며 연결된 모든 객체를 추적(Tracing)한다. 손이 닿는 모든 살아있는 객체에 마킹(Mark)을 한 뒤, 마킹되지 않은 나머지 찌꺼기 객체들을 빗자루로 쓸어 담듯(Sweep) 날려버린다.
  • 순환 참조 문제도 깔끔하게 해결하지만, 힙 메모리 전체를 훑어야 하므로 연산 오버헤드가 제법 크고, 지운 자리에 구멍이 숭숭 뚫려 메모리가 단편화(Fragmentation)되는 문제가 있어 이를 모으는 압착(Compaction) 과정이 병행된다.1

3. 모든 자바 엔지니어의 원수: Stop-The-World (STW)

가비지 컬렉터가 구동될 때, 객체의 참조 주소가 마구 바뀌는 대혼란을 방지하기 위해 런타임 엔진은 GC 스레드를 제외한 애플리케이션의 모든 스레드를 일시적으로 정지시킨다. 이를 Stop-The-World (STW)라고 부른다.

3.1. STW 극복을 위한 런타임 튜닝

  • 아무리 고상한 코드라도 STW가 발동하면 0.1초에서 심하면 수 초 동안 서비스가 먹통이 된다. 특히 실시간 초단타 금융 거래 시스템이나 온라인 멀티플레이어 게임 서버에서 STW가 수 초간 터지면 고객들의 쌍욕 섞인 환불 문의가 날아온다.
  • 이를 해결하기 위해 JVM은 메모리를 Young 영역(금방 생성되었다 사라지는 객체)과 Old 영역(오래 살아남는 객체)으로 나누어 부분 청소(Minor GC)를 진행하는 세대별 가비지 컬렉션(Generational GC) 기법을 활용한다. 또한 현대 자바 진영은 일시 정지 시간을 10ms 이하로 극단적으로 단축한 G1 GC, ZGC 등 초고사양 쓰레기 수거 알고리즘을 도입하여 치열한 생존 전투를 벌이고 있다.2

4. 관련 밈 및 드립

4.1. OOM (Out Of Memory) 엔딩

서버가 잘 돌아가다가 뜬금없이 'java.lang.OutOfMemoryError: Java heap space' 에러를 내뿜으며 비명횡사하는 상황이다. 가비지 컬렉터가 어떻게든 힙 메모리를 비워보려고 죽어라 STW를 걸어가며 개삽질(Full GC)을 반복하다가, 결국 '하... 더는 못 비우겠다'라며 프로세스를 강제 종료시키고 서버가 기절해 버린다. 백엔드 개발자들은 아침에 출근하여 로그에서 OOM 한 줄을 발견하는 순간 지옥을 경험한다.

4.2. Rust의 비웃음

런타임 엔진에 얹혀살며 메모리 가비지 수거 렉을 겪는 자바/자바스크립트 개발자들을 보며, 러스트 개발자들이 던지는 우월한 미소다. Rust는 GC도 없고 수동 해제도 없는 독특한 소유권(Ownership) 및 빌림 검사(Borrow Checker) 규칙을 컴파일 단계에서 강제하여, 런타임 지연이 단 1밀리초도 없는 초고속 실시간 메모리 정리를 달성해 전산계의 기염을 토했다.

5. 여담

  • 메모리 릭의 숨은 주범, 전역 변수: 자바스크립트나 자바에서 쓸데없는 전역 변수에 거대한 배열이나 객체를 할당해 두면, 이 전역 변수가 계속 Root Set 역할을 하므로 쓰지 않음에도 영원히 GC가 비워주지 못하고 메모리를 좀먹게 된다.
  • 가장 사치스러운 청소: C/C++ 기반의 고성능 커스텀 엔진들 중 일부는 게임 스테이지가 바뀔 때나 연산이 끝난 타이밍에 단순히 GC 알고리즘을 돌리는 것이 아니라, 메모리 풀(Memory Pool) 자체를 날려버리는 상남자식 초고속 청소(?)를 즐기기도 한다.
  • 가장 오래된 GC 기술: Lisp 언어의 창시자 존 매카시가 1959년 수학 연구 도중 손코딩을 아끼기 위해 고안해 냈다. 컴퓨터 구조도 제대로 확립되지 않았던 극초창기에 현대식 오토 메모리 기법의 정수를 깨달은 천재의 통찰이 엿보이는 부분이다.

6. 관련 문서

각주

  1. 메모리 틈새 구멍이 많아지면 연속된 큰 객체를 메모리에 올릴 자리가 없어 용량이 남아돌아도 OOM이 터질 수 있기 때문이다.

  2. 이 과정에서 백엔드 엔지니어들은 JVM의 -XX:+UseG1GC 같은 기괴한 커맨드 라인 옵션 수십 개를 조율하며 오르가즘(?)을 느끼곤 한다.