Python의 Garbage Collecting

Hansu Kim·2021년 12월 30일
0

개발 Must-know

목록 보기
5/9

GC - Garbage Collector

OS는 프로그램을 프로세스로 실행하게 되고, 해당 프로세스는 메모리에 코드,데이터,힙,스택 영역을 할당받게 된다.

이 때, 힙, 스택 영역에 할당된 메모리들을 해제하는 동작을 Garbage Collector가 수행하게 된다.

Garbage Collector의 동작은 각 언어별로 언제/어떻게 동작하는지 차이점이 있으며, 특히 동적 객체가 할당되는 힙 영역에서의 동작은 반드시 숙지하고 있어야 한다.

Python의 GC 동작 방식

Python의 Garbage Collecting은 다음 2 가지 방식으로 동작한다.

  • Reference counting
  • Generational garbage collection

1. Reference counting

레퍼런스 카운팅 방식의 가비지 컬렉팅은, 어떤 객체가 참조되고있는 횟수를 카운팅하고 0이 될 경우 메모리에서 해제하는 방식이다.

python standard library의 sys모듈로 특정 객체의 reference counts를 확인할 수 있다.

>>> import sys
>>> a = 'hello'
>>> sys.getrefcount(a)
2

위 코드에서, 'hello'는 힙에 할당되고 a는 지역변수라는 가정 하에 스택에 할당되어 'hello'가 위치한 힙의 주소를 갖고 있다.

이 상황에서 a의 레퍼런스 카운트는 2이다.
첫번째는 a가 위치한 스택의 주소가 콜스택에서 참조되고 있기 때문이며, 두번째는 getrefcount()에 전달될 때이다.

만약 콜스택에서 a가 위치한 레벨의 함수가 실행 완료되면, 콜스택에서 참조하고 있는 a가 사라지기에 reference count는 0이되고 gc에 의해 메모리에서 해제된다.

2. Generational garbage collection

>>> l = []
>>> l.append(l)
>>> del l

위 코드의 l과 같이, 만약 어떤 동적 객체가 서로를 참조(순환참조)하고 있다면 Reference counting 방식에서는 해당 객체들은 메모리에서 해제될 수 없다.

이러한 경우를 방지하기 위해, 파이썬에서는 Generational garbage collection이 순환 참조를 탐지하고 메모리에서 해제해준다.

어떻게 순환 참조를 탐지하는가

순환 참조는 컨테이너 객체(e.g. tuple, list, set, dict, class)에 의해서만 발생한다. 컨테이너 객체들만이 다른 객체의 참조를 보유할 수 있기 때문이다.

순환 참조 해결을 위해 아래의 순서로 동작한다.

  1. 각 객체의 gc_refs 필드를 레퍼런스 카운트와 같게 설정
  2. 각 객체에서 참조하고 있는 다른 컨테이너 객체를 찾고, 참조되는 컨테이너의 gc_refs를 감소시킨다.
  3. 어느 객체의 gc_refs가 0이 되면, 그 객체는 컨테이너 집합 내부에서 자기들끼리 참조하고 있다는 뜻이다.
  4. 그 객체를 unreachable하다고 표시한 뒤 메모리에서 해제한다.

어떤 기준으로 가비지 컬렉션이 발생하는가

Generational garbage collection을 이해하기 위해서는 아래 세대와 Threshold 개념에 대해 인지하고 있어야 한다.

1. Generation

Python GC는 객체를 0~2세대로 분리하여 관리하고, 세대가 낮을 수록 더욱 자주 garbage collecting을 수행한다.
이 동작은 어린 객체가 오래된 객체보다 해제될 가능성이 높다는 가설에서 근거한다.

2. Threshold

세대별 객체의 수가 정해진 Threshold를 초과하면, 임계치가 초과된 세대의 객체에 대해 수행하게 된다.

Threshold를 통한 세대별 가비지 컬렉팅 동작 예시

세대별 Threshold는 gc모듈의 get_threshold()를 통해 확인할 수 있다.

>>> import gc
>>> gc.get_threshold()
(700, 10, 10)
>>> gc.get_count()
(121, 9, 2)

위 코드를 기준으로,
(700, 10, 10)에서 threshold 0(700)의 의미는 0세대 객체가 700개를 초과하면 가비지 컬렉팅이 수행된다는 의미이다. gc모듈은 0세대 객체가 threshold 0을 초과하면 가비지컬렉팅을 수행하고, 남아있는 객체들을 1세대 객체로 옮기며, 1세대의 count를 1 증가 시킨다.

threshold 1(10)의 의미는 약간 다르다.
threshold 0은 0세대 객체 수에 대한 임계값이었지만, threshold 1은 0세대 가비지컬렉팅이 발생한 횟수에 대한 임계치이다.

threshold 2(10) 역시, 1세대 가비지컬렉팅이 발생한 횟수에 대한 임계값이다.

즉, 0세대 가비지 컬렉팅이 객체생성 700번만에 발생한다면,
1세대는 7000번 만에, 2세대는 70000번 만에 가비지 컬렉션이 수행된다는 의미이다.

GC모듈의 사용

레퍼런스 카운팅 방식은 python에서 자동으로 수행되며,
파이썬의 가비지 컬렉터는 세대별 가비지 컬렉션만을 수행한다. 세대별 가비지 컬렉션은 코드에서 아래와 같이 사용할 수 있다.

  • gc.get_count() : 각 세대의 객체 수 확인
  • gc.set_threshold() : 세대별 임계치 설정
  • gc.collect_generations() : 모든 세대에 대해서 2세대부터 0세대까지의 순서로 확인하고, 임계치 초과시 collect() 호출
  • gc.collect() : 가비지 컬렉션 수행하여 순환참조 객체를 메모리에서 해제

참조URL:
https://dgkim5360.tistory.com/entry/understanding-the-global-interpreter-lock-of-cpython
인스타그램의 GC 비활성화 https://luavis.me/python/dismissing-python-garbage-collection-at-instagram
https://blog.winterjung.dev/2018/02/18/python-gc

0개의 댓글