일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 홍콩과기대김성훈교수
- 딥러닝
- 스택
- 정렬
- Python
- 파이썬
- DP
- 파이토치
- pytorch
- 강의정리
- classifier
- 백준
- DynamicProgramming
- machine learning
- AI
- 자연어처리
- loss
- 강의자료
- Hypothesis
- 머신러닝 기초
- 머신러닝
- Natural Language Processing with PyTorch
- BAEKJOON
- rnn
- Softmax
- MSE
- Cross entropy
- 알고리즘
- tensorflow
- Deep learning
- Today
- Total
개발자의시작
파이썬(python) 메모리 관리 2 본문
파이썬은 어떻게 메모리 관리를 하는가?
- 파이썬은 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')
a = RefExam()
print(f'count {sys.getrefcount(a)}')
b = a
print(f'count {sys.getrefcount(a)}')
c = a
print(f'count {sys.getrefcount(a)}')
c = 0
print(f'count {sys.getrefcount(a)}')
b = 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)}')
a = RefExam()
a = 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)}')
a = RefExam()
a = 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/ 의 글을 정리한 글입니다.
'프로그램언어 > python' 카테고리의 다른 글
파이썬(python) 메모리 관리 1 (0) | 2021.04.30 |
---|---|
파이썬 클래스(class) 이해하기 2 (0) | 2020.06.17 |
파이썬 클래스(class) 이해하기 1 (0) | 2020.06.17 |