개발언어/PYTHON

[Python] Garbage collector(가비지 컬렉터)란 ?

to,min 2024. 11. 9. 23:28

 

막연히 Python 가비지 컬렉터가 동작함은 알고 있었지만, 내부적으로 어떤 기준에서 동작하는지 에 대한 질문을 받아

정리해보려 한다.

난 이 정보를 알아보기 전까지 막연히 Java와 비슷하게 동작하는 줄 알았는데, 꽤 다른 것 같다...

 

Garbage collector(가비지 컬렉터) ?

우선 Garbage collector 란  메모리 관리 기법 중의 하나로, 프로그램이 동적으로 할당했던 메모리 영역 중에서 필요 없게 된 영역을 해제하는 기능이다.

예를 들어 A라는 객체를 생성하면, 해당 객체는 메모리를 점유하고 있는데, 그중 사용하지 않는 혹은 우선순위가 밀리는 객체의 메모리를 해제하는 것이다

 

Garbage collector 동작 방식 - Python

기본적으로 파이썬은 아래 두 가지 방식으로 GC를 수행한다.

 

1. Reference Counting 

2. Generational collection

 

 

Reference Counting 

객체를 참조하는 횟수를 세는 것이다. 객체를 참조할 때마다 카운팅이 증가되고, 참조가 해제될 때 카운트가 감소한다.

카운트가 0이 되면 객체를 참조하는 부분이 없으므로, 메모리에서 제거한다.

--> 순환 참조 시에는 객체가 서로 참조하고 있으니, 카운트가 0이 될 리가 없어 동작하지 않는 단점이 있다.

 

 

아래 코드는 Reference Counting 예시 코드이다.

getrefcount 함수는 실행할 때, a에 대한 참조를 한번 하며 확인하는 구조라 실제론 +1 이 되어 결과가 나온다.

또한 파이썬 가비지컬렉터는 Reference Counting이 0이 되자마자 동작하는 것이 아니라, 백그라운드에서 돌기 때문에

특정 시점을 코드로 확인하긴 어렵지만, 동작 확인 차 예시로 del를 기입해 두었다.

 

import sys

class MyClass:
    def __del__(self):
        print(f"{self} 객체가 메모리에서 제거되었습니다.")

# 객체 생성
a = MyClass()
print("초기 레퍼런스 카운트:", sys.getrefcount(a))  # 초기 레퍼런스 카운트 확인

# 참조 추가
b = a
print("b에 할당 후 레퍼런스 카운트:", sys.getrefcount(a))

# 참조 제거
del b
print("b 제거 후 레퍼런스 카운트:", sys.getrefcount(a))

# 최종 참조 제거
del a  # 이때 __del__ 메서드가 호출되어 객체가 제거됩니다.

 

실행결과

 

 

Generational collection

그럼 reference counting 만으로 해결할 수 있겠지만, 앞서 말했던 것과 같이, 순환참조가 되면 동작을 안 하는 문제가 있다.

아래는 순환 참조 예시 코드이다.

 

import sys
import gc

class NodeA:
    def __del__(self):
        print("NodeA 객체가 메모리에서 제거되었습니다.")

class NodeB:
    def __del__(self):
        print("NodeB 객체가 메모리에서 제거되었습니다.")

# 객체 생성 및 상호 참조
a = NodeA()
b = NodeB()
a.other = b
b.other = a

# 참조 제거
del a
del b

 

언듯 본다면 a, b를 지워서 참조가 해제된다고 볼 수 있지만, 실상 해당 코드는 reference counting 이론 가비지 컬렉터에서 동작하지 않는다.

 

a.other 에서 b를 참조하고, b.other 에서 a를 참조하고 있는 순환참조 구조이다. 

저렇게 되면 a를 del 하고 b를 del 해도 a 객체와 b 객체 내부의 other 속성에 서로를 참조하고 있어. reference count 가 0이 되지 않는다.

del 함수가 좀 헷갈릴 수 있는데, del은 객체 자체를 지운다기보단, 변수에 할당한 객체를 해제시키는 개념으로 보면 이해가 될 것이다.

보통의 경우는 변수에 할당한 객체를 해제시키면 파이썬 가비지 컬렉터가 해당 객체의 메모리를 해제시킨다.

 

따라서 파이썬에선 순환 참초 시에도 가비지 컬렉터를 동작시키기 위한 방법론인 Generational collection 방식을 사용한다.

 

Generational collection은 간단히 말하면, 객체를 세대별로 관리하고, 해당 세대의 임계값(Threshold)을 넘으면 GC를 수행하는 것이다.

 

아래 코드는 각 세대 별 임계 값을 나타내는 코드이다.

import gc
gc.get_threshold()

 

실행 결과

 

0세대는 700, 1세대는 10, 2세대는 10으로 이뤄져 있고, 0세대의 객체가 700개가 넘으면 0세대 GC 가 수행되고,

1세대는 0세대의 GC가 10번 수행되면 수행되고, 2세대는 1세대 GC가 10번 수행되면 2세대 GC 수행된다. 

 

0세대에서 1세대로 가는 방법은 0세대 GC에서 살아남으면 된다( 살아남는 객체가 강한 객체이다?)

참고로 이때 사이클 감지 등을 통하여 순환 참조로 인한 reference counting은 해제되고 진짜 참조하고 있는 객체들만 위로 올라간다.

 

즉 초기 객체가 700개 생성되면 0세대 GC 가 일어나고 그게 10번 즉 단순 산수로는 7000번 이상 객체 할당이 이뤄져야 1세대 GC가 1번 일어난다.

 

무튼 이런 식으로 파이썬 가비지 컬렉터는 두 가지 방식으로 동작하니, 참고하면 좋을 것 같다!

 

 

참고

https://medium.com/dmsfordsm/garbage-collection-in-python-777916fd3189

 

Garbage Collection in Python

Python의 메모리 관리 기법을 알아보자.

medium.com