Programming Language/Python

Garbage Collection in Python, 파이썬의 GC에 대해

TwinParadox 2021. 6. 6. 15:04
728x90

Python에서의 GC

파이썬은 기본적으로 레퍼런스 카운팅(Reference Counting)을 바탕으로 GC를 수행하고 메모리를 관리한다. 

 

레퍼런스 카운팅(Reference Counting)?

모든 객체는 참조 당할 때 이 레퍼런스 카운트를 증가시키고, 참조가 없어지면 이를 감소시킨다. 이 값이 0이 되면 객체가 메모리에서 해제된다. 해당 객체의 레퍼런스 카운트 값을 확인하는 코드는 다음과 같다.

sys.getrefcount(obj)

 

GC의 동작 원리

앞서 언급한 것처럼 GC는 레퍼런스 카운트를 기준으로 진행하는데, 좀 더 정확히 다루자면 세대와 그에 따른 임계값을 바탕으로 주기적으로 관리한다. 여기서 말하는 세대는 숫자가 클수록 오래된 객체이며, GC는 기본적으로 0세대, 즉, 비교적 최근에 생성된 객체에 대해 자주 GC를 수행하게 된다.

 

파이썬 GC에서 사용하는 세대는 0, 1, 2 세 가지고, 각각의 임계값은 다음 코드로 확인할 수 있고, 설정도 가능하다. 각각의 임계값을 초과하면 GC가 수행된다.

 

th0, th1, th2 = gc.get_threshold()
gc.set_threshold(1000, 100, 100)

 

  • 0세대, 메모리에 객체 할당 횟수에 해제된 횟수를 뺀 값으로, 객체 수를 의미한다. 임계값을 초과하면 0세대 GC를 수행한다.
  • 1세대, 0세대 GC 이후 살아남은 객체를 1세대로 이동시킨 후 카운터를 1 증가시키며, 이 1세대 카운터가 임계값을 초과하면 1세대 GC를 수행한다.
  • 2세대 GC는 1세대 GC처럼, 1세대 GC에서 살아남은 객체들에 동일한 방식으로 GC를 수행한다.

 

GC 수행 과정

GC가 결국 세대와 임계값을 바탕으로 수행되기 때문에, GC의 실행 과정을 아는 것이 좋다.

 

  1. 새로운 객체가 생성되면, 메모리와 0세대에 객체를 할당한다. 이 때, 객체 수가 0세대 임계값보다 크면 collect_generations()가 호출된다.
  2. collect_generations()은 0, 1, 2세대 모두에 대해서 검사를 수행하며 2세대부터 역순으로 진행한다. 
  3. 각 세대에 대해 임계값을 검사한 후 GC를 수행한다. 

 

이렇게 threshold에 근거해서 GC를 수행하지만, 수동으로 GC를 수행하는 과정이 필요할 수가 있다. 이런 경우, gc.collect()로 수동 실행이 가능하다.

 

import gc
collected = gc.collect()
print(collected)

 

threshold에 의존한 GC가 충분하지 않을 때 수동으로 GC를 수행시킬 수 있다. gc.collect()는 순환 참조 탐지 알고리즘을 바탕으로 도달 가능 객체(rechable), 도달 불가능 객체(unrechable)로 구분한다. 이 때, 도달 가능 객체는 세대 이동을 시키고, 도달 불가능 객체는 콜백 수행 후 메모리에서 해제된다.

gc.collect()가 반환하는 값은 점유된 객체 숫자와 메모리가 해제된 객체 숫자를 의미한다.

 

 

 

 

순환 참조?

다음과 같은 코드가 있다고 하자.

 

자기 참조

a = []
a.append(a)
del a

 

상호 참조

a = Obj()
b = Obj()

a.x = b
b.x = a

del a
del b

 

이와 같은 상황이 순환 참조라고 할 수 있다. 만약 위 상황이라고 하면 레퍼런스 카운팅 방식에서는 레퍼런스 카운트가 0이 아니라서 메모리에서 해제되지 않는다. 파이썬은 이를 해결하기 위해서 Cyclic GC를 지원한다. 참조 주기를 감지해서 이를 처리하며, 다음과 같은 절차를 거친다.

 

  1. 객체에 gc_refs 필드를 레퍼런스 카운트와 동일하게 설정 
  2. 각 객체에 참조하고 있는 다른 컨테이너 객체를 찾고, 참조되는 컨테이너의 gc_refs를 감소시킴
  3. gc_ref가 0이면, 그 객체는 컨테이너 집합 내부에서 자기들끼리 참조하고 있음
  4. 참조 불가능 객체로 표시하고 메모리에서 해제

 

 

그래서 어떻게 하는 것이 좋은데?

정확한 답을 내리기는 어렵다.

간단한 어플리케이션이라면 자동 GC로도 충분히 해결이 가능하지만, 로직들이 많고 사용하는 라이브러리가 커지면 커질수록 의도하지 않은 곳에서(자신이 작성한 코드거나, 라이브러리 자체의 문제거나) 수동 GC가 필요할 수가 있다. 이럴 때는 시간 경과에 따라, 이벤트 수행 결과 또는 시스템 상태에 맞춰 수동으로 GC를 수행해주는 것이 좋겠다.

그렇다고 해서, 수동이 무조건 답도 아닐 뿐더라 GC가 늘 좋은 성능을 보장하지는 않는다. 인스타그램의 경우 GC 없애는 방법으로 성능 튜닝을 이끌어내기도 했다. 이런 사례들을 보니 파이썬의 GC에 대한 접근은 뭔가 좀 더 많은 경험과 실험이 필요할 것 같다.

 

 

Reference

https://docs.python.org/ko/3.7/library/gc.html

https://weicomes.tistory.com/277

https://luavis.me/python/dismissing-python-garbage-collection-at-instagram

728x90