개발자의시작

파이썬(python) 메모리 관리 2 본문

프로그램언어/python

파이썬(python) 메모리 관리 2

LNLP 2021. 4. 30. 05:53

파이썬은 어떻게 메모리 관리를 하는가?

- 파이썬은 C/C++과 같이 프로그래머가 직접 메모리 관리를 하지 않고 레퍼런스 카운트(Reference Counts)와 가비지 컬렉션(Automatic Garbage Collection)에 의해 관리됩니다.

 

레퍼런스 카운트(Reference Counts)

- 파이썬은 내부적으로 malloc()와 free()를 많이 사용하기 때문에 메모리 누수의 위험이 있습니다. 이런 이슈가 있기 때문에 파이썬은 메모리를 관리하기 위한 전략으로 레퍼런스 카운트를 사용합니다.

 

- 레퍼런스 카운트란 파이썬의 모든 객체에 카운트를 포함하고, 이 카운트는 객체가 참조될 때 증가하고, 참조가 삭제될 때 감소시키는 방식으로 작동됩니다. 이때 카운트가 0이 되면 메모리가 할당 해제됩니다.

 

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
import sys
 
class RefExam():
  def __init__(self):
    print('create object')
 
= RefExam()
print(f'count {sys.getrefcount(a)}')
= a
print(f'count {sys.getrefcount(a)}')
= a
print(f'count {sys.getrefcount(a)}')
= 0
print(f'count {sys.getrefcount(a)}')
= 0
print(f'count {sys.getrefcount(a)}')
 
"""
OUT PUT:
count 2
count 3
count 4
count 3
count 2
"""
 
 

- 생성 직후 레퍼런스 카운트가 2가 출력되는 것을 확인할 수 있는데, 이는 getrefcount()의 파라미터 값으로 임시 참조되기 때문입니다.

- 첫 번째 출력 이후 b, c에 각각 참조될 때마다 1씩 증가하는 것을 확인 할 수 있습니다. 그리고 b, c에 0이 할당될 때 1씩 감소하는 것을 확인할 수 있습니다.

- 레퍼런스 카운트는 메모리 관리에 효율적으로 동작하지만, 카운트만으로 메모리를 관리했을때 약점이 있습니다.

 

레퍼런스 카운트의 약점(순환 참조)

- 순환 참조란 간단하게 컨테이너 객체가 자기 자신을 참조하는 것을 말합니다. 자기 자신이 참조될 때 프로그래머는 할당된 객체를 추적하기 어려워지고, 이때 메모리 누수가 발생할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class RefExam():
  def __init__(self):
    print('create object')
  def __del__(self):
    print(f'destroy {id(self)}')
 
 
= RefExam()
= 0
print('end .....')
 
"""
OUT PUT:
create object
destroy 3112733520336
end .....
"""
 

__del__()은 메모리 할당이 삭제되는 시점에 실행되는 메소드입니다. 위 코드와 같이 a 변수에 0을 재할당 할 때 __del__()이 실행되고 마무리하는 것을 확인할 수 있습니다. 하지만 여기서 순환 참조가 될 때는 아래 예제처럼 출력됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# me 프로퍼티에 자기 자신을 할당합니다.
class RefExam():
  def __init__(self):
    print('create object')
    self.me = self
  def __del__(self):
    print(f'destroy {id(self)}')
 
= RefExam()
= 0
print('end .....')
 
"""
OUT PUT:
create object
end .....
destroy 2110595412432
"""
 

- 위 코드는 자기 자신을 할당하기 전과 다르게 'end....'를 출력하고 __del__()이 실행되는 것을 확인 할 수 있습니다.

- 변수 a에 새로운 값을 할당해도 a.me 속성에 자기 자신을 참조하고 있어 레퍼런스 카운트가 남아있기 때문에 이런 현상이 발생합니다.

- 이렇게 되면 레퍼런스 카운트가 0에 도달할 수 없고 할당된 메모리를 삭제할 수 없어 메모리 누수가 발생합니다.

- 파이썬은 이 문제를 가비지 컬렉션으로 해결합니다.

 

가비지 컬렉션

레퍼런스 카운트도 가비지 컬렉션이다.

- 설명에 들어가기에 앞서 레퍼런스 카운트도 가비지 컬렉션(GC)라고 부릅니다. 이를 구분하기 위해서 순환 참조 이슈를 해결하기 위해 구현한 가비지 컬렉션을 'Automatic Garbage Collection' 이라고 부릅니다.

- 파이썬에서는 'Cyclic Garbage Collection'을 지원합니다. 이는 순환 참조 이슈를 해결하기 위해 존재하며, 참조 주기를 감지하여 메모리 누수를 예방합니다.

 

Generational Hypothesis

- 가비지 컬렉션은 Generational Hypothesis라는 가설을 기반으로 작동합니다. 이 가설은 "대부분의 객체는 생성되고 오래 살아남지 못하고 곧바로 버려지는 것"과 "젊은 객체가 오래된 객체를 참조하는 것은 드물다"는 2가지 가설입니다.

- 이 가설을 기반으로 메모리에 존재하는 객체를 오래된 객체(old)와 젊은 객체(young)으로 나눌 수 있는데, 대부분의 객체는 생성되고 곧바로 버려지기 때문에 젊은 객체에 비교적 더 많이 존재한다고 볼 수 있습니다.

- 즉, Generational Hypothesis를 기반으로 작동한다는 것은 젊은 객체에 대부분의 객체가 존재하니, 가비지 컬렉터가 작동 빈도수를 높여 젊은 객체 위주로 관리해주는 것입니다.

 

세대관리

- 파이썬은 객체 관리를 위한 영역을 3가지로 나눕니다. 이 영역을 세대(generation)라고 합니다. 파이썬에서 세대를 초기화할 때 아래의 _PyGC_Initialize 메소드를 호출하는 데 3세대를 초기화하는 것을 확인할 수 있습니다.

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
#define NUM_GENERATIONS 3                 /* 3세대로 관리 */
 
// ...
 
#define GEN_HEAD(state, n) (&(state)->generations[n].head)
 
// ...
 
void
_PyGC_Initialize(struct _gc_runtime_state *state)
{
    state->enabled = 1/* automatic collection enabled? */
 
  #define _GEN_HEAD(n) GEN_HEAD(state, n)
    struct gc_generation generations[NUM_GENERATIONS] = {
        /* PyGC_Head,                                           threshold,    count */
        \{\{(uintptr_t)_GEN_HEAD(0), (uintptr_t)_GEN_HEAD(0)\},   700,        0\},      /** 0세대 초기화 */
        \{\{(uintptr_t)_GEN_HEAD(1), (uintptr_t)_GEN_HEAD(1)\},   10,         0\},      /** 1세대 초기화 */
        \{\{(uintptr_t)_GEN_HEAD(2), (uintptr_t)_GEN_HEAD(2)\},   10,         0\},      /** 2세대 초기화 */
    };
    for (int i = 0; i < NUM_GENERATIONS; i++) {
        state->generations[i] = generations[i];
    };
    
  // ...
}
 
 

- 코드를 초기화할 때 임계값(threshold)을 각 700, 10, 10으로 초기화하고 카운트(count)를 0,0,0으로 초기화합니다.

- 파이썬 런타임 환경에서 설정된 임계값과 현재 카운트 확인을 gc.get_threshold()와 gc.get_count()로 확인할 수 있습니다.

1
2
3
4
5
6
7
import gc
print(gc.get_threshold())
print(gc.get_count())
"""
OUTPUT:
(700, 10, 10)
(18, 7, 8)              // 현재 count상태를 확인하는 것이기 때문에 출력값이 다를 수 있다.
 

- 파이썬은 이렇게 셋팅한 세대별 임계값과 할당된 카운트를 비교하여 컬렉션을 결정합니다.

- 임계값을 활용하는 방법은 객체가 생성될 때 0세대의 카운트 값이 증가합니다. 증가될 때 0세대의 카운트와 임계값을 비교하여 만약 카운트가 임계값보다 클 때 쓰레기 수집을 실행하고 0세대는 초기화됩니다.

- 0세대의 살아남은 객체는 다음 1세대로 옮겨지고 1세대의 카운트(count)는 1 증가합니다. 

- 이런 방식으로 젊은 세대(young)에서 임계값이 초과되면 오래된 세대(old)로 위임하는 방식으로 3세대 영역으로 관리됩니다. 

 

- 가비지 컬렉션은 내부에서 컬렉션 대상 이하의 세대 카운트를 초기화하고, 도달 가능(reachable)한 객체와 도달할 수 없는(unreachable) 객체를 분류합니다.

- 그리고 분류된 도달할 수 없는 객체들을 메모리에서 삭제합니다.

 

 

이 글은 dc7303.github.io/python/2019/08/06/python-memory/ 의 글을 정리한 글입니다.

Comments