가비지 컬렉션(garbage collection)은 자동으로 메모리 관리를 수행하는 과정이다. 사용되지 않는 메모리 영역을 식별하고, 해제하여 메모리를 재사용 가능하게 만드는 것이 목적이다.
파이썬의 메모리 관리는 다음과 같은 흐름으로 진행된다.
앞의 두 과정은 이전 포스트에서 다루었는데, 간단히 살펴보면, 파이썬은 기본적으로 객체를 메모리에 할당할 때 레퍼런스 카운트 방식을 사용한다. 각 객체 혹은 어떤 변수는 다른 객체에 의해 참조될 때마다 카운트가 증가하게 된다. 레퍼런스 카운트가 0이 될 경우 가비지 컬렉터는 해당 객체를 메모리에서 해제하게 된다.
이때 두 개 이상의 객체가 서로를 참조하는 경우 레퍼런스 카운트가 0이 되지 않는 상황이 발생하는데, 이를 순환 참조라고 한다. 이러한 경우 자동으로 메모리에서 해제되지 않기 때문에, 순환 참조를 탐지하고 해결하기 위해 세대별 가비지 컬렉션이라는 방법을 도입한다.
파이썬의 세대별 가비지 컬렉터는 특정 전제 하에 작동한다. 대부분의 객체는 짧은 시간 동안만 존재하고, 따라서 새로운 객체가 오래된 객체보다 메모리 해제될 가능성이 높다는 방향성으로, 아래와 같이 3가지의 세대로 나누어 관리한다.
각 세대별로 가비지 컬렉션이 언제 발생할지 결정하는 임계값이 존재한다. 해당 세대에 할당된 객체 수와 관련이 있으며, 다음과 같은 방식으로 임계값을 사용한다.
아래와 같이 gc 모듈을 사용하여 가비지 컬렉션 동작 및 통계 등을 직접 확인할 수도 있다.
import gc
>>> gc.get_threshold()
(700, 10, 10)
>>> (gc.get_count()
(167, 5, 1)
컨테이너 객체란 다른 객체들에 대한 참조를 보유할 수 있는 객체다. 예를 들어 튜플, 리스트, 집합, 딕셔너리, 클래스 등이 있다. 문자열의 경우 컨테이너 타입이라고 할 수는 있지만, 다른 객체에 대한 참조를 저장하지 않기 때문에 순환 참조의 고려 대상은 아니다. 정수와 같은 기본 데이터 타입 역시 고려 대상이 아니다.
gc 모듈을 활용하면 collect() 메서드를 통해 가비지 컬렉션(순환 참조 탐지 알고리즘 포함)을 수행할 수 있다. 그렇다면 순환 참조를 어떻게 탐지할 수 있을까? 대략적인 과정은 아래와 같다.
위의 설명대로라면 순환 참조를 일으키지 않는 컨테이너 객체들은 추적할 필요가 없다. 파이썬은 가비지 컬렉션의 비용을 줄이기 위해 일부 객체들을 추적에서 제외한다. 물론, 제외할 객체를 결정하는 것도 비용이기 때문에 비용의 이득 관계를 고려하여 다음과 같은 두 가지 전략을 사용한다.
일반적으로 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
https://devguide.python.org/internals/garbage-collector/
https://blog.winterjung.dev/2018/02/18/python-gc
https://medium.com/dmsfordsm/garbage-collection-in-python-777916fd3189