Notice
Recent Posts
Recent Comments
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
10-11 00:15
Archives
Today
Total
관리 메뉴

Developer_Neo

[python] 가비지컬렉션과 레퍼런스 카운트 본문

프로그래밍/Python

[python] 가비지컬렉션과 레퍼런스 카운트

_Neo_ 2022. 1. 18. 11:42
반응형

기존 메모리 관리의 문제점

  • 필요 없는 메모리를 비우지 않았을 때
    • 메모리 사용을 마쳤을 때 비우지 않을 경우 메모리 누수가 발생
    • 장기적인 관점에서 심각한 문제가 발생
  • 존재하지 않는 메모리에 접근하려고 하면 프로그램이 중단되거나 메모리 데이터 값이 손상될 수 있다

이러한 문제를 해결하기 위해 현대적인 언어는 자동 메모리 관리(Automatic Memory Management)를 갖추게 되었다.

 

파이썬에선 기본적으로 Garbage Collection(가비지 컬렉션)과 reference counting(레퍼런스 카운팅)을 통해 할당된 메모리를 관리한다

 

가비지컬렉션(Garbage Collection)

소멸 규칙 및 과정을 이야기하는 것으로 메모리를 자동으로 관리해주는 과정이다 

 

레퍼런스 카운트

참조 횟수(reference counts)를 증가시키는 방법

  • 변수에 객체 할당
  • list에 추가하거나 class instance에서 속성으로 추가하는 등의 data structure에 객체 추가.
  • 객체를 함수의 인수로 전달

모든 객체는 참조 당할 때 레퍼런스 카운터를 증가시키고 참조가 없어질 때 카운터를 감소시킨다. 이 카운터가 0이 되면 객체가 메모리에서 해제한다

객체의 레퍼런스 카운트를 보고 싶다면 sys.getrefcount()로 확인할 수 있다.

 

 

가비지컬렉션 작동 방식

r = [1,2,3] # r이라는 변수로 리스트를 참조

r = 'simple' # 변수 r이 참조대상을 문자열로 바꿨다.
'''
이때 리스트 [1,2,3]은 언제 소멸되는가?
소멸되지 않고 계속 메모리공간을 차지한다면 위에서 말한 문제가 생길수 있고 
자동적 메모리관리 시스템을 파이썬이 가지고 있으니...

리스트 [1,2,3]은 객체고 상태는 객체(리스트)를 아무도 참조하지 않는 상황이다.
이때 리스트 [1,2,3]은 소멸 대상이 된다
그런데 소멸 대상이 되었다고 바로 소멸되는 것이 아니라 
소멸 대상으로 등록만 해두고 시스템에 시간적인 여유가 생길 때 소멸시키게 된다.
'''


'''
레퍼런스 카운트 중심으로 봐보자
'''

r1 = [1,2,3]  # 리스트 [1,2,3]의 레퍼런스 카운트는 1
r2 = r1  # 리스트의 레퍼런스 카운트는 2 
r1 = 'simple'  # 리스트의 레퍼런스 카운트는 1로 감소, simple의 레퍼런스 카운트는 1
r2 = 'happy'  # 리스트의 레퍼런스 카운트는 0으로 감소, happy의 레퍼런스 카운트는 1

즉 레퍼런스 카운트는 객체를 참조하는 변수의 수를 가리킨다.

레퍼런스 카운트가 0이 되었다는 것은 소멸대상으로 등록이 된 것이다.

 

 

그러면 가비지컬렉션은 누가 진행하는가?

일단 파이썬 코드를 작성하고 실행하면 이 코드는 바이트 코드라는 것으로 변환되어 어딘가에 저장된다. 그리고 이 바이트 코드는 파이썬 가상머신(Python Virtual Machine)위에서 실행 된다.

파이썬 프로그램의 실행 주체PVM파이썬 가상머신이라 할 수 있고 PVM에 의해 가비지 컬렉션이 진행된다.

 

그리고 파이썬 코드 변환기, 가상머신, 기본적으로 포함되는 각종라이브러리들을 묶어 파이썬 인터프리터라고 한다.

 

 

가비지 컬렉션에서 문제가 생길 수 있는 부분

1. 객체가 순환 참조할 경우

    순환 참조객체가 자기 자신을 가르키는 것을 말한다.

>>> import sys
>>> a = []
>>> sys.getrefcount(a)
2
>>> a.append(a)
>>> a
[[...]]
>>> sys.getrefcount(a)
4
>>> sys.getrefcount(a)
3
>>> del a
>>> a
Traceback (most recent call last):
  File "<pyshell#12>", line 1, in <module>
    a
NameError: name 'a' is not defined
>>> sys.getrefcount(a)
Traceback (most recent call last):
  File "<pyshell#13>", line 1, in <module>
    sys.getrefcount(a)
NameError: name 'a' is not defined

여기에서 2개의 특징을 알았다. 

- getrefcount에 a를 인자로 전달을 하게 되어 실제로 참조횟수는 1인데 참조횟수가 1이 증가된 2로 된다는 것.

- 프롬프트상에 a를 전달하면 참조횟수가 그때 늘어난다는 것이다.

   위의 결과로 프롬프트에 궁금한 점이 생겨 찾아보았지만 딱히 왜 그런지는 찾지 못했다...

 

결과

a의 참조 횟수는 1 이지만 이 객체는 더이상 접근할 수 없으며 레퍼런스 카운팅 방식으로는 메모리에서 해제될 수 없다.

 

2. 서로를 참조하는 객체

>>> a = po() # 0x01
>>> b = po() # 0x02
>>> a.x = b # 0x01의 x는 0x02를 가리킨다.
>>> b.x = a # 0x02의 x는 0x01를 가리킨다.
# 0x01의 레퍼런스 카운트는 a와 b.x로 2다.
# 0x02의 레퍼런스 카운트는 b와 a.x로 2다.
>>> del a # 0x01은 1로 감소한다. 0x02는 b와 0x01.x로 2다.
>>> del b # 0x02는 1로 감소한다.

 

1번과 2번 둘다 del키워드를 썼는데 del키워드는 특징들이 있다. 

del키워드 사용시 참조는 할 수 없다.

파이썬 공식문서를 보면 del키워드 사용시 객체변수는 참조할 수 없도록 제거되지만 메모리에 공간은 남아있는 상태가 된다.

이렇기 때문에 래퍼런스카운팅에 대한것도 1로 남아있게 된다. 하지만 del키워드를 사용한 객체에 접근할 수 있는 방법은 없는데 래퍼런스카운팅이 계속 1로 남아있게 되어  이에 대한 메모리가 해제 될수가 없어 문제가 생기게 되는 것이다.

 

 

위의 문제를 reference cycle(참조 주기)라고 하며 이것을 해결하고자 파이썬에서는 Generational garbage collection이 순환 참조를 탐지하고 메모리에서 해제해준다

 

Generational Garbage Collector

가비지 컬렉터는 내부적으로 generation(세대)과 threshold(임계값)로 가비지 컬렉션 주기와 객체를 관리한다.

 

generation(세대)

- 가비지 컬렉터는 메모리의 모든 객체를 추적한다. 새로운 객체는 0세대 가비지 수집기에서 life(수명)를 시작한다.

- Python이 세대에서 가비지 수집 프로세스를 실행하고 객체가 살아남으면, 두 번째 이전 세대로 올라간다. Python 가비지 수집기는 총 3세대이며, 객체는 현재 세대의 가비지 수집 프로세스에서 살아남을 대마다 이전 세대로 이동한다

최근에 생성된 객체는 0 세대(young)에 들어가고 오래된 객체일수록 2 세대(old)에 존재한다.

 

threshold(임계) 값

- 주기와 관련이 있다.

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

- 각 세대마다 가비지 컬렉터 모듈에는 임계값 개수의 개체가 있습니다. 객체 수가 해당 임계값을 초과하면 가비지 콜렉션이 콜렉션 프로세스를 trigger(추적) 합니다. 해당 프로세스에서 살아남은 객체는 이전 세대로 옮겨진다.

'''
gc 모듈을 사용하며 get_threshold() method를 사용하여 가비지 컬렉터의 구성된 임계값을 확인할 수 있다.
'''
>>> import gc
>>> gc.get_threshold()
(700, 10, 10)
'''
각각 threshold 0, threshold 1, threshold 2를 의미
'''

 n세대에 객체를 할당한 횟수가 threshold n을 초과하면 가비지 컬렉션이 수행된다.

 

0세대의 경우 메모리에 객체가 할당된 횟수에서 해제된 횟수를 뺀 값, 즉 객체 수가 threshold 0을 초과하면 실행된다. 다만 그 이후 세대부터는 조금 다른데 0세대 가비지 컬렉션이 일어난 후 0세대 객체를 1세대로 이동시킨 후 카운터를 1 증가시킨다. 이 1세대 카운터가 threshold 1을 초과하면 그때 1세대 가비지 컬렉션이 일어난다. 비약시켜서 0세대 가비지 컬렉션이 객체 생성 700번만에 일어난다면 1세대는 7000번만에, 2세대는 7만번만에 일어난다는 뜻이다.

 

가비지 컬렉터는 세대와 임계값을 통해 가비지 컬렉션의 주기를 관리한다. 

가비지 컬렉션의 실행 과정(라이프 사이클)

새로운 객체가 만들어질때 파이썬은 객체를 메모리와 0세대에 할당한다.

만약 0세대의 객체 수가 threshold 0보다 크면 collect_generations()를 실행한다.

collect_generations() 메서드가 호출되면 모든 세대(기본적으로 3개의 세대)를 검사하는데 가장 오래된 세대(2세대)부터 역으로 확인한다.

해당 세대에 객체가 할당된 횟수가 각 세대에 대응되는 threshold n보다 크면 collect()를 호출해 가비지 컬렉션을 수행한다.

collect() 메서드는 순환 참조 탐지 알고리즘을 수행하고 특정 세대에서 도달할 수 있는 객체(reachable)와 도달할 수 없는 객체(unreachable)를 구분하고 도달할 수 없는 객체 집합을 찾는다.

도달할 수 있는 객체 집합은 다음 상위 세대로 합쳐지고(0세대에서 수행되었으면 1세대로 이동), 도달할 수 없는 객체 집합은 콜백을 수행한 후 메모리에서 해제된다.

 

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

순환 참조는 컨테이너 객체(e.g. tuple, list, set, dict, class)에 의해서만 발생할 수 있다

순환 참조를 해결하기 위한 아이디어로 모든 컨테이너 객체를 추적한다

객체 내부의 링크 필드에 더블 링크드 리스트를 사용하는 방법이 가장 좋다.

이렇게 하면 추가적인 메모리 할당 없이도 컨테이너 객체 집합에서 객체를 빠르게 추가하고 제거할 수 있다.

컨테이너 객체가 생성될 때 이 집합에 추가되고 제거될 때 집합에서 삭제된다.

 

순환 참조를 찾는 과정

  1. 객체에 gc_refs 필드를 레퍼런스 카운트와 같게 설정한다.
  2. 각 객체에서 참조하고 있는 다른 컨테이너 객체를 찾고, 참조되는 컨테이너의 gc_refs를 감소시킨다.
  3. gc_refs가 0이면 그 객체는 컨테이너 집합 내부에서 자기들끼리 참조하고 있다는 뜻이다.
  4. 그 객체를 unreachable 하다고 표시한 뒤 메모리에서 해제한다.
a = [1]
b = ['a']
c = [a, b]
d = c
# 컨테이너 객체가 생성되지 않았기에 레퍼런스 카운트만 늘어난다.

e = Po(0)
f = Po(1)
e.x = f
f.x = e

del e
del f


'''
각 컨테이너 객체의 레퍼런스 카운트 ref count
'''
[1]     <- a,c      = 2
['a']   <- b,c      = 2
[a, b]  <- c,d      = 2
Po(0)  <- Po(1).x = 1
Po(1)  <- Po(0).x = 1

'''
1번 과정에서 각 컨테이너 객체의 gc_refs가 설정된다.
gc_refs
'''
[1]    = 2
['a']  = 2
[a, b] = 2
Po(0) = 1
Po(1) = 1


'''
2번 과정에서 컨테이너 집합을 순회하며 gc_refs을 감소시킨다.

[1]     = 1  # [a, b]에 의해 참조당하므로 1 감소
['a']   = 1  # [a, b]에 의해 참조당하므로 1 감소
[a, b]  = 2  # 참조당하지 않으므로 그대로
Po(0)  = 0  # Po(1)에 의해 참조당하므로 1 감소
Po(1)  = 0  # Po(0)에 의해 참조당하므로 1 감소
'''



'''
3번 과정을 통해 gc_refs가 0인 순환 참조 객체를 발견했다. 
이제 이 객체를 unreachable 집합에 옮겨주자.

unreachable |  reachable
             |    [1] = 1
 Po(0) = 0  |  ['a'] = 1
 Po(1) = 0  | [a, b] = 2
'''

 

 

참고한 블로그 https://medium.com/dmsfordsm/garbage-collection-in-python-777916fd3189

                  https://blog.winterjung.dev/2018/02/18/python-gc

참고한 책 : 윤성우의 열혈파이썬 중급편

반응형
Comments