다 쓴 데이터는 누가, 언제 치울까요? 창고를 도는 청소부는 아무도 찾지 않는 상자만 가져가고, 누군가 보관증을 쥐고 있는 상자에는 손대지 않습니다.
프로그램이 새 데이터를 만들 때마다 힙에서 빈 자리를 얻습니다. 이것을 할당이라고 합니다. 그런데 다 쓴 데이터를 아무도 치우지 않으면 힙은 결국 가득 차고, 프로그램은 더 이상 할당하지 못해 죽습니다. 이 단원은 "다 썼다"를 컴퓨터가 어떻게 판단하는지, 그리고 그 판단과 청소를 대신해 주는 GC(가비지 컬렉터)가 무슨 일을 하는지 봅니다. 사진을 불러와 썸네일을 만드는 짧은 반복문을 한 단계씩 따라가 보겠습니다.
컴퓨터는 프로그래머의 의도를 모릅니다. 대신 아주 기계적인 기준을 씁니다: 지금 살아 있는 변수들에서 출발해 참조를 따라갔을 때 닿을 수 있으면 살아 있는 데이터, 어떻게 해도 닿을 수 없으면 쓰레기(garbage). 예를 들어 변수 img에 새 사진의 주소를 덮어쓰는 순간, 이전 사진 상자는 아무도 가리키지 않게 됩니다. 그 데이터가 지워진 게 아니라, 닿을 수 없게 된 것입니다. 애니메이션에서 흐려지며 취소선이 그어지는 상자가 바로 그 순간입니다.
가장 기본적인 GC는 두 단계로 동작합니다. 먼저 스택의 변수들(뿌리)에서 출발해 참조를 타고 다니며 닿을 수 있는 상자에 전부 표시를 합니다(mark). 그다음 표시가 없는 상자를 회수해 빈 공간으로 되돌립니다(sweep). 그래서 GC는 "쓸모없어 보이는 것"을 치우는 게 아니라 정확히 "닿을 수 없는 것"만 치웁니다.
표시하는 동안 프로그램이 참조를 마구 바꾸면 계산이 어긋나므로, GC는 보통 프로그램을 아주 잠깐 멈춰 세우고 일합니다. 이 멈춤을 GC pause라고 부릅니다. 힙이 클수록, 살아 있는 데이터가 많을수록 이 멈춤이 길어집니다. 애니메이션에서 CPU 표시가 잠시 "대기"로 바뀌는 구간이 그것입니다. 서버에서 응답 시간이 가끔 튀는 이유 중 하나가 여기에 있습니다.
GC가 있는 언어에서도 메모리는 찹니다. 이유는 두 가지뿐입니다. 첫째, 만드는 속도가 회수 속도를 앞지를 때. 둘째, 누군가가 참조를 계속 쥐고 있어서 GC가 회수할 자격을 못 얻을 때입니다. 캐시에 계속 쌓아두거나, 끝난 요청의 데이터를 리스트에 남겨두거나, 오래 사는 객체가 짧게 살아야 할 데이터를 붙잡고 있는 경우가 전부 후자입니다. 이 두 번째 경우를 흔히 retention(과점유)이라 부릅니다.
1main() {2 album = []PC ◀3 for i in 0..2 {4 img = load("photo" + i)5 thumb = resize(img)6 album.push(thumb)7 }8 show(album)9}
빈 배열이 힙에 만들어지고(@1), album이 그 번호표를 듭니다. 이번 단원의 질문은 하나입니다 — 메모리는 왜 차는가.
시뮬레이터에서 해보기
이 단원의 용어
왜 이렇게 발전했나
예전에는 프로그래머가 직접 "이제 이 메모리 반납할게(free)"라고 적어야 했습니다. 그런데 이게 인류가 가장 많이 틀린 코드였습니다 — 두 번 반납하거나, 반납한 걸 또 쓰거나, 반납을 깜빡하거나(누수). 그래서 대부분의 현대 언어가 도달 가능성을 기준으로 자동 회수하는 GC를 택했습니다. 대가는 pause와 약간의 CPU 오버헤드입니다.
GC는 닿을 수 없는 것만 가져간다 — 쥐고 있으면 절대 못 가져간다.
남은 문제
그럼 무엇이 참조를 오래 쥐게 만들까요? 다음 단원에서 만날 "기다리는 코드"가 그 주범 중 하나입니다 →