[Python] Garbage Collection

dony·2024년 2월 10일
0

Python

목록 보기
2/4
post-thumbnail

Garbage Collection 이란?

가비지 컬렉션(garbage collection)은 자동으로 메모리 관리를 수행하는 과정이다. 사용되지 않는 메모리 영역을 식별하고, 해제하여 메모리를 재사용 가능하게 만드는 것이 목적이다.

Python의 GC

파이썬의 메모리 관리는 다음과 같은 흐름으로 진행된다.

  1. 레퍼런스 카운팅 (Reference Counting)
  2. 순환 참조 (Reference Cycle)
  3. 세대별 가비지 컬렉션 (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() 메서드를 통해 가비지 컬렉션(순환 참조 탐지 알고리즘 포함)을 수행할 수 있다. 그렇다면 순환 참조를 어떻게 탐지할 수 있을까? 대략적인 과정은 아래와 같다.

  1. 파이썬은 모든 컨테이너 객체 추적을 위해 더블 링크드 리스트를 사용한다. PyGC_Head라는 구조체에 정의되어 있으며, 컨테이너 객체가 생성 혹은 삭제될 때 이 리스트에 추가되거나 제거된다.
  2. 각 객체는 gc_refs라는 필드를 가지고 있으며, 초기에 객체의 레퍼런스 카운트와 동일하게 설정된다.
  3. 각 객체에서 참조하고 있는 다른 컨테이너 객체를 찾아 참조되는 컨테이너의 gc_refs를 감소시킨다. gc_refs가 0이 되는 경우 해당 객체는 컨테이너 집합 내부에서 순환 참조를 이루고 있음을 의미한다.
  4. 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

Reference

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

0개의 댓글