
[Python] Garbage Collection
Garbage Collection 이란?
가비지 컬렉션(garbage collection)은 자동으로 메모리 관리를 수행하는 과정입니다. 사용되지 않는 메모리 영역을 식별하고, 해제하여 메모리를 재사용 가능하게 만드는 것이 목적입니다.
Python의 GC
파이썬의 메모리 관리는 다음과 같은 흐름으로 진행됩니다.
- 레퍼런스 카운팅 (Reference Counting)
- 순환 참조 (Reference Cycle)
- 세대별 가비지 컬렉션 (Generational Garbage Collection)
앞의 두 과정은 이전 포스트에서 다루었는데, 간단히 살펴보면, 파이썬은 기본적으로 객체를 메모리에 할당할 때 레퍼런스 카운트 방식을 사용합니다. 각 객체 혹은 어떤 변수는 다른 객체에 의해 참조될 때마다 카운트가 증가하게 됩니다. 레퍼런스 카운트가 0이 될 경우 가비지 컬렉터는 해당 객체를 메모리에서 해제하게 됩니다.
이때 두 개 이상의 객체가 서로를 참조하는 경우 레퍼런스 카운트가 0이 되지 않는 상황이 발생하는데, 이를 순환 참조라고 합니다. 이러한 경우 자동으로 메모리에서 해제되지 않기 때문에, 순환 참조를 탐지하고 해결하기 위해 세대별 가비지 컬렉션이라는 방법을 도입합니다.
Generational Garbage Collector
세대 (Generation)
파이썬의 세대별 가비지 컬렉터는 특정 전제 하에 작동합니다. 대부분의 객체는 짧은 시간 동안만 존재하고, 따라서 새로운 객체가 오래된 객체보다 메모리 해제될 가능성이 높다는 방향성으로, 아래와 같이 3가지의 세대로 나누어 관리합니다.
- 세대 0: 가장 최근에 생성된 객체들이 속합니다. 컬렉션이 자주 작동합니다.
- 세대 1: 세대 0에서 살아남은 객체들이 이동합니다. 컬렉션이 세대 0보다 드물게 작동합니다.
- 세대 2: 가장 오래된 객체들이 속한 세대입니다. 컬렉션이 가장 드물게 작동합니다.
임계값 (Threshold)
각 세대별로 가비지 컬렉션이 언제 발생할지 결정하는 임계값이 존재합니다. 해당 세대에 할당된 객체 수와 관련이 있으며, 다음과 같은 방식으로 임계값을 사용합니다.
- 각 세대에 객체가 추가될 때마다 카운트가 증가합니다.
- 어떤 세대의 객체 수(카운트)가 해당 세대 임계값을 초과하면, 해당 세대에 대한 가비지 컬렉션이 실행됩니다. 가비지 컬렉션 프로세스에서 살아남은 객체의 경우 다음 세대로 이동합니다.
- 세대 0의 가비지 컬렉션 수행은 나머지 임계값에도 영향을 줍니다. 세대 0의 가비지 컬렉션이 일정 횟수 수행되면, 세대 1에서도 실행되고, 세대 2에도 영향을 미치는 식입니다.
아래와 같이 gc 모듈을 사용하여 가비지 컬렉션 동작 및 통계 등을 직접 확인할 수도 있습니다.
import gc
>>> gc.get_threshold()
(700, 10, 10)
>>> (gc.get_count()
(167, 5, 1)순환 참조와 컨테이너 객체
컨테이너 객체란 다른 객체들에 대한 참조를 보유할 수 있는 객체입니다. 예를 들어 튜플, 리스트, 집합, 딕셔너리, 클래스 등이 있습니다. 문자열의 경우 컨테이너 타입이라고 할 수는 있지만, 다른 객체에 대한 참조를 저장하지 않기 때문에 순환 참조의 고려 대상은 아닙니다. 정수와 같은 기본 데이터 타입 역시 고려 대상이 아닙니다.
gc 모듈을 활용하면 collect() 메서드를 통해 가비지 컬렉션(순환 참조 탐지 알고리즘 포함)을 수행할 수 있습니다. 그렇다면 순환 참조를 어떻게 탐지할 수 있을까요? 대략적인 과정은 아래와 같습니다.
- 파이썬은 모든 컨테이너 객체 추적을 위해 더블 링크드 리스트를 사용합니다. PyGC_Head라는 구조체에 정의되어 있으며, 컨테이너 객체가 생성 혹은 삭제될 때 이 리스트에 추가되거나 제거됩니다.
- 각 객체는 gc_refs라는 필드를 가지고 있으며, 초기에 객체의 레퍼런스 카운트와 동일하게 설정됩니다.
- 각 객체에서 참조하고 있는 다른 컨테이너 객체를 찾아 참조되는 컨테이너의 gc_refs를 감소시킵니다. gc_refs가 0이 되는 경우 해당 객체는 컨테이너 집합 내부에서 순환 참조를 이루고 있음을 의미합니다.
- gc_refs가 0인 객체는 unreachable로 설정하고, 메모리에서 해제합니다.
최적화
위의 설명대로라면 순환 참조를 일으키지 않는 컨테이너 객체들은 추적할 필요가 없습니다. 파이썬은 가비지 컬렉션의 비용을 줄이기 위해 일부 객체들을 추적에서 제외합니다. 물론, 제외할 객체를 결정하는 것도 비용이기 때문에 비용의 이득 관계를 고려하여 다음과 같은 두 가지 전략을 사용합니다.
- 컨테이너가 생성될 때
- 가비지 컬렉터가 컨테이너를 검사할 때
일반적으로 atomic한 인스턴스의 경우 추적하지 않고, 그렇지 않은 인스턴스는 추적합니다. 다만, 경우에 따라 동작 최적화가 다르게 적용됩니다. gc 모듈의 is_tracked(obj) 함수를 사용하면 객체의 추적 상태를 확인할 수 있습니다.
>>> gc.is_tracked(0)
False
>>> gc.is_tracked("a")
False
>>> gc.is_tracked([])
True
>>> gc.is_tracked({})
False
>>> gc.is_tracked({"a": 1})
False
>>> gc.is_tracked({"a": []})
True