가비지 컬렉션
Garbage collection. 줄여서 GC라고도 많이 쓴다.
말 그대로 해석해 보면 '쓰레기 모으기'라는 뜻이다. 프로그래밍 언어에서 쓰이는 개념으로, 메모리에서 사용하지 않는 '쓰레기'를 모아서 폐기하는 것을 뜻한다. 이러한 일을 수행하는 프로그램을 가비지 컬렉터(garbage collector)라고 부른다.
이러한 쓰레기의 대표적인 사례는 동적으로 배당 받은 메모리가 있다. C나 C++[1]는 동적으로 받은 메모리를 다 사용하고 나면 반드시 반납해야 하며, 반납하지 않은 메모리는 계속 프로그램이 잡고 있기 때문에 프로그래머의 실수로 메모리를 반납하지 않으면 메모리 누수가 일어난다. 예를 들어 동적 메모리를 사용하는 어떤 함수가 이를 반납하지도, 반환값으로 내보내지도 않고[2] 종료된다면 그 함수를 호출할 때마다 메모리 누수가 발생할 것이다. 이러한 누수가 쌓이면 심각한 버그를 단골로 일으킨다.
가비지 컬렉션을 지원하는 언어는 코드에서 직접 반납하지 않은 메모리라고 해도 더 이상 사용하지 않는다고 판단하면 수집해서 시스템에 반납한다. 예를 들어 어떤 함수 안에서 동적으로 메모리를 배당 받았는데, 이 함수 안에서 반납하지도 않고, 함수의 반환값으로 내보내지도 않은 메모리가 있다면 이는 분명히 '쓰레기'다. 자바나 C#을 비롯해서 가비지 컬렉션을 지원하는 언어에서는 알고리즘으로 볼 때 명백하게 쓰레기로 판단할 수 있는 메모리들을 수집한다. 따라서 코드에서 직접 메모리를 반납하지 않아도 되며, 아예 반납 명령 자체가 없는 언어도 많다. 또한 이미 반납한 메모리를 다시 반납하려고 한다든가, 반납한 메모리를 또 반납하려고 한다든가 하는 버그 역시도 예방할 수 있다.
가비지 컬렉션은 프로그래머가 메모리 배당과 반납에 신경쓰지 않아도 되므로 편리하고 버그도 적지만 성능으로 보면 약점이 있다. 가비지 컬렉터는 뒷단에서 계속 메모리 배당을 감시하고 배당 받은 메모리를 관리하고 있다가 일정한 주기로 쓰레기를 수집해서 폐기하는 일을 한다. 따라서 가비지 컬렉터가 돌아가는 만큼 컴퓨터의 성능을 소비하며, 쓰레기를 수집하고 폐기하는 주기에 처리해야 할 쓰레기가 많다면 이를 처리하는 과정에서 프로그램의 실행이 눈에 띄게 느려지거나 잠시 멈추는 경우도 있다. 비유하자면 쓰레기를 만든 사람이 직접 쓰레기통까지 들고 가서 버리지 않고 그냥 길바닥에 버려도 청소차가 정기적으로 돌면서 쓰레기를 주워가는 시스템이라고 할 수 있다. 사람들은 귀찮게 쓰레기를 들고 쓰레기통까지 가는 수고를 안 해도 돼서 편리하지만 청소차가 돌아다니기 전까지는 여기 저기에 쓰레기가 널려 있을 것이고, 청소차가 돌아다니다가 도로에 멈춰서 쓰레기를 주워담다 보면 교통량이 많은 도로나 좁은 도로에서는 교통체증이 일어날 것이다. 이 때문에 정밀한 실시간 처리가 중요하거나 속도가 절대 중요한 소프트웨어는 C와 같이 가비지 컬렉션을 사용하지 않는 언어를 선호한다. 모질라재단이 밀고 있는 Rust라는 언어는 가비지 컬렉션을 사용하지 않고 컴파일 때에 메모리 누수를 모두 잡아낼 수 있도록 언어를 디자인했다.
주요 전략
포인터 추적
어떤 객체를 한 개 이상의 변수가 참조하고 있다면 접근 가능(accessible), 즉 사용 중이고 그렇지 않다면 접근 불가능, 즉 사용하지 않는 것으로 간주하여 접근 불가능한 객체는 메모리에서 해제하는 전략이다.
참조 회수 카운트
C++의 스마트 포인터에서 쓰는 전략이다. 메모리를 변수에 동적으로 배당할 때, 여기에 참조 회수를 카운트하는 변수를 하나 붙여 준다. 만약 이 변수의 초기값이 1이라고 하면, 이 변수를 다른 변수에서 참조할 때마다 카운트가 1씩 늘어나고, 참조를 해제하면 카운트가 1씩 줄어든다. 참조 카운트가 1이 되었을 때 참조가 해제되면 메모리를 시스템에 반납한다.
별도의 가비지 컬렉터가 계속 감시하고 있을 필요가 없고 참조가 일어날 때 또는 참조가 해제될 때에 곧바로 반응하므로 메모리 관리 효율 면에서는 가장 좋다. 그러나 참조가 일어날 때마다 더하기 또는 빼기 연산이 일어나기 때문에 그만큼 성능을 잠식하는 문제다. 더 문제가 되는 것은 두 객체가 서로를 참조하고 있을 때에 일어나는 순환 참조다. a, b 객체가 서로를 참조하고 있다고 가정하면 두 객체 모두 다른 곳에서 전혀 참조하고 있지 않아도 서로가 참조하고 있기 때문에 참조 카운터가 1이기 때문에 메모리가 반납되지 않는다.