Home [Python] Python Garbage Collector
Post
Cancel

[Python] Python Garbage Collector

개요

안녕하세요.

이번 글에서는 Python의 Garbage Collector에 대해 살펴보겠습니다.

현재 글은 CPython(Cython)을 바탕으로 작성되었으며 Pypy, Jython 등은 다른 동작을 보일 수 있습니다.


Garbage Collector(GC)의 개념

파이썬에서의 메모리 할당과 해제

파이썬 메모리 정리 글에서 살펴볼 수 있지만 파이썬의 모든 객체는 힙에 동적으로 할당됩니다.

또한 객체의 사용이 끝나면 메모리를 반환합니다.

그렇다면 파이썬에서는 객체의 사용이 끝났다는 사실을 어떻게 확인할까요?

Garbage Collector

Garbage Collector(이하 GC)는 프로그램이 동적으로 할당했던 메모리 영역 중 필요없게 된 메모리를 해제하는 기능입니다.

GC는 다음과 같은 버그를 방지할 수 있습니다.

  • 유효하지 않은 포인터 접근: 이미 해제된 메모리에 접근하는 버그입니다. 만약 이 포인터가 해제되고 새로운 값이 할당되었다면, 잘못된 값을 읽어오게 됩니다.
  • 이중 해제: 이미 해제된 메모리를 또다시 해제하는 버그입니다. 일부 메모리 할당 알고리즘에서는, 해제된 메모리를 다시 해제하려고 시도하는 과정에서 오류를 일으킬 수 있습니다.
  • 메모리 누수: 더이상 필요하지 않은 메모리가 해제되지 않고 남아있는 버그입니다. 메모리 누수가 반복되면 메모리 고갈로 프로그램이 중단될 수 있습니다. (접근 가능한 메모리가 증가하여 메모리가 고갈되는 문제는 GC로 막을 수 없다고 합니다.)

하지만 마냥 장점만 존재하지 않는데 GC에는 다음과 같은 단점이 존재합니다.

  • 어떤 메모리를 해제할지 결정하는 데 리소스가 소요됩니다. 객체가 필요없어지는 시점을 프로그래머가 미리 알고 있는 경우에도 GC가 메모리 해제 시점을 추적해야 하므로, 이 작업은 오버헤드가 됩니다.
  • 쓰레기 수집이 일어나는 타이밍이나 점유 시간을 미리 예측하기 어렵습니다. 따라서 프로그램이 예측 불가능하게 일시적으로 정지할 수 있습니다. 실시간 시스템에서는 GC의 적용을 고려해볼 수 있습니다.
  • 할당된 메모리가 해제되는 시점을 알 수 없습니다.

파이썬 GC의 동작원리

파이썬에서도 GC를 이용해 동적 메모리를 관리합니다.

GC는 Reference Count와 Cyclic Reference라는 2가지 방법으로 대상을 선별하고 할당 해제합니다.

① Reference Count

첫 번째는 참조 횟수입니다.

파이썬의 모든 객체는 참조 횟수를 지닙니다.

어떤 객체의 참조 횟수가 0이 되었을 때 해당 객체는 메모리에서 할당 해제됩니다.

다음 예제 코드를 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
import sys
import gc


a = "123"
b = [1, 2, 3]

print("==========")
print(sys.getrefcount(a))  # 4
print(gc.get_referrers(a))
print("==========")
print(sys.getrefcount(b))  # 2
print(gc.get_referrers(b))

sys.getrefcount 함수는 객체의 참조 횟수를 반환하며, 내부적으로 객체를 복사하므로 (실제 참조 횟수) + 1을 반환합니다.

gc.get_referrers 함수는 객체를 참조하는 객체의 리스트를 반환합니다.

그렇다면 위의 코드에서 변수 a는 3번, 변수 b는 1번 참조되는 것을 알 수 있습니다.

여기서 변수 a의 참조 횟수와 관련하여 몇 가지 의견을 찾아볼 수 있었지만 명확한 답을 내리지 못했습니다.

관련하여 도움이 될 수 있는 링크와 의견을 남깁니다.

  1. 참조횟수가 4로 시작하는 이유: https://stackoverflow.com/questions/45021901/why-does-a-newly-created-variable-in-python-have-a-ref-count-of-four
  2. 지역변수: locals() 함수 통해 확인 가능
  3. getefcount 함수: 공식 문서의 https://docs.python.org/3.11/library/sys.html#sys.getrefcount
  4. peephole optimazor: co_consts 키워드, /Python/peephole.c -> /Python/compile.c로 이동한 optimazor 코드 (https://github.com/python/cpython/pull/21517/commits)
  5. 컴파일 과정: 컴파일 과정에서 전체 코드를 리스트로 저장

② Cyclic Reference

두 번째는 순환참조입니다.

순환참조가 발생한 객체를 확인하여 해당 객체를 메모리에서 할당 해제합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import gc

class Link:
   def __init__(self, next_link=None):
       self.next_link = next_link

link_3 = Link()
link_2 = Link(link_3)
link_1 = Link(link_2)
link_3.next_link = link_1
A = link_1
del link_1, link_2, link_3

link_4 = Link()
link_4.next_link = link_4
del link_4

# Collect the unreachable Link object (and its .__dict__ dict).
gc.collect()

위 코드의 경우, [link_1, link_2, link_3]link_4에서 각각 순환참조가 발생합니다.

GC는 다음의 과정을 통해 순환참조를 확인하고 메모리 할당 해제를 수행합니다.

⓪ 시작 전 알아둘 사실

먼저 gc_ref 값으로 GC 알고리즘 시작 단계에서 참조 횟수를 저장하는 변수와 동일한 값의 변수(다음 그림에서는 gc_ref)를 초기화합니다.

두번째는 객체의 상태 변경으로 GC는 도달할 수 없는(다음 그림에서는 unreach) 객체만 변경합니다.

이는 “대부분의 객체는 참조할 수 있다”는 가정 하에서 유용한 접근 방식입니다.

① GC 수행 전

순환참조 01 GC 수행 전 [그림01] 순환참조 01 GC 수행 전

GC 수행 이전 객체별 참조 상태입니다.

실제 참조 횟수와 동일한 값을 가지는 gc_ref 값을 확인할 수 있습니다.

② gc_ref 값 감소

순환참조 02 [그림02] 순환참조 02 gc_ref 값 감소

GC를 수행하면 모든 객체의 gc_ref를 1씩 감소시킵니다.

여기서 Object to Scan 목록의 외부에 참조가 있는 객체만 gc_ref > 0에 해답합니다.

gc_ref가 0인 객체는 참조할 수 없는 상태, 즉 UNREACH 상태가 될 수 있습니다.

이 그림에서는 link_2, link_3, link_4 객체가 해당됩니다.

③ UNREACH 상태로 변경

순환참조 03 [그림03] 순환참조 03 unreach 상태로 변경

여기서 link_3, link_4 객체만 UNREACH 상태로 변경됩니다.

link_2 객체의 경우 gc_ref 값이 0이지만 link_1 객체에 의해 외부에서 참조되기 때문입니다.

④ REACHABLE 상태 객체에서 접근 및 조건에 맞는 객체의 상태 변경

순환참조 04 [그림04] 순환참조 04 REACHABLE 상태 변경

위 작업이 끝나면 link_1 객체를 스캔해서 이로부터 도달할 수 있는 모든 객체를 찾습니다.

따라서 link_3 객체의 상태를 UNREACH에서 REACHABLE 상태로 변경합니다.

이 과정에서 GC는 PREV_MASK_COLLECTING flag를 통해 각 객체를 1번씩만 방문합니다.

결과적으로 link_4 객체는 도달할 수 없는 다시말해 순환참조하는 객체이며 이 객체의 메모리는 할당 해제됩니다.


마무리하며

이번 글에서는 파이썬의 GC를 살펴보았습니다.

GC는 동적으로 할당된 메모리를 해제하는 역할을 수행합니다.

파이썬 GC는 참조 횟수와 순환참조를 확인하는 방법을 통해 동적 메모리를 할당합니다.

이 글이 조금이나마 도움이 되셨으면 합니다.

감사합니다. 😀


참고 문헌

This post is licensed under CC BY 4.0 by the author.

[Python] Python Memory 관리

[CS] 프로세스와 스레드 01 - 프로세스의 기본 개념

Comments powered by Disqus.