(Python) 파이썬 메모리 관리
파이썬 메모리 관리
파이썬은 메모리를 직접 관리하지 않기 때문에 메모리에 대한 신경을 다른 언어에 비해 덜 쓸 수 있다. 파이썬에서는 레퍼런스 카운트(Reference Counts)와 가비지 콜렉션(Garbage Collection)에 의해 관리된다
레퍼런스 카운트
파이썬은 내부적으로 malloc()과 free() 함수를 많이 사용하기 때문에 메모리 누수(동적 할당 되는 메모리의 크기는 런타임 내내 변하기 때문에 사용이 끝난 메모리를 해제하지 않으면 메모리가 부족해지는 현상)의 위험이 있다. 그렇기 때문에 메모리를 관리하기 위해서 사용된다
레퍼런스 카운트는 파이썬의 모든 객체에 카운트를 포함하고 객체가 참조될때 카운트가 증가, 참조가 삭제될때 카운트를 감소시키는 방식으로 작동된다. 객체의 reference count가 0이 되면 객체의 메모리 할당이 해제된다
malloc()
동적으로 메모리를 할당하는 함수 로 힙 영역에 메모리를 할당하는 함수
free()
힙 영역에 할당받은 메모리 공간을 다시 os로 반환해주는 함수이다
특정 메모리 주소를 참조하는 곳의 수가 0이 될 경우 다음 GC때 메모리에서 해제가 된다
레퍼런스 카운트는 getrefcount()를 통해 파라미터로 전달된 객체의 카운트를 확인할 수 있다
(여기서 처음 출력되는 카운트 값이 1이 아닌 이유는 a의 객체가 getrefcount함수의 인자로 넘어가기 때문에 인자로 넘겨주는 순간 +1 이 된다)
import sys
a = 'hello'
print(f'count {sys.getrefcount(a)}')
b = a
print(f'count {sys.getrefcount(a)}')
c = b
print(f'count {sys.getrefcount(a)}')
b = 0
print(f'count {sys.getrefcount(a)}')
"""
count 2
count 3
count 4
count 3
"""
순환참조시 레퍼런스 카운트 문제
이때 레퍼런스 카운트에는 문제가 존재한다. 순환참조시 a의 참조횟수는 1이지만 이 객체에는 더이상 접근할 수 없고 레퍼런스 카운트 방식으로는 메모리에서 해제될 수 없다
a = []
a.append(a)
del a
순환참조
객체가 자기 자신을 가리키는 것을 말한다
이때 가비지 컬렉션을 사용하면 된다.
Generational Garbage Collector
gc는 내부적으로 세대 와 임계값 을 통해서 가비지 컬렉션 주기와 객체를 관리한다. 총 3세대가 있으며 최근에 생성된 객체는 0세대로 들어가고 오랜된 객체는 이전 세대로 이동한다.
(최근 생성된 객체는 0세대에 들어가고 오래된 객체일수록 2세대로 이동된다. 그 이유는 최근에 만들어진 객체가 오래된 객체보다 해제될 가능성이 훨씬 높다는 가설이 있기 때문이다)
-
generation(세대) : 가비지 컬렉터는 메모리의 모든 객체를 추적하는데 새로운 객체는 1세대 가비지 수집기에서 수명을 시작한다. 가비지 컬렉션이 실행되고 객체가 남아있으면 해당 객체는 두번째 이전 세대로 올라간다.
-
threshold(임계값) : 각 세대마다 즉 각 제너레이션마다 가비지 커렉터 모듈에는 임계값 개수의 개체가 있다. 객체 수가 임계값을 초과하면 가비지 컬렉션이 실행되고 이 과정에서 살아남은 객체는 이전 세대로 옮겨진다.
가비지 컬렉션을 사용하기 위해서 GC module을 사용한다
메모리 할당 내부 동작
- PyObject_GC_MALLOC()
- collect_generations()
gc의 set_threshold()는 가비지 컬렉터의 구성된 임계값을 확인할 수 있다. 만약 아래처럼 임계값이 3인 경우라고 가정하자(각 제너레이션, 즉 각 세대에 들어갈 수 있는 객체의 최대값)
import gc
gc.set_threshold(3)
값이 할당되면 파이썬 내부적으로 PyObject_GC_Malloc() 함수가 호출되어 hi라는 값은 메모리에 할당되게 된다. 이후 해당 함수는 할당된 메모리 주소를 리턴하게 되고 메모리 주소는 a로 들어가게 된다. 제너레이터(0세대)에는 hi라는 값이 들어가게 된다.
a = 'hi'
PyObject_GC_Malloc(--)
b라는 변수에 how라는 값을 넣었고 마찬가지로 PyObject_GC_Malloc() 함수가 호출되어 how라는 값은 메모리에 할당되게 된다. 이후 how값은 제너레이터(0세대)에 들어가게 된다
b = 'how'
PyObject_GC_Malloc(--)
c라는 변수에 are라는 값을 넣었고 PyObject_GC_Malloc() 함수가 호출되어 are라는 값은 메모리에 할당되게 된다. 이후 are값은 제너레이터(0세대)에 들어가게 된다.
c = 'you'
PyObject_GC_Malloc(--)
이후 you라는 값이 함수 호출에 의해 메모리에 할당되는데 제너레이터의 임계값은 3이기 때문에 you가 들어가면 threshold를 초과하게 된다.
더이상 0세대에는 변수를 할당할 수 없게 된다.
이때는 PyObject_GC_Malloc()함수에서 collect_generations(–)라는 함수를 호출한다 collect_generstions은 이전 세대부터 각 객체가 몇개 들어가 있는지 체크한다. 체크한뒤 초과가 된 제너레이션을 찾아 초과가 된 세대에 가비지 컬렉터를 돌리라는 명령을 한다.
collect(0)
collect함수는 0세대에 있는 모든 값들을 불러온 뒤 값의 레퍼런스 카운트를 체크한다. 레퍼런스 카운트가 0일 경우 값을 메모리에서 지우게 된다. 0이 아닌 경우 해당 값은 위의 제너레이션 즉 1세대로 올리게 된다. 즉 1세대에는 hi, how, are라는 값들이 저장되고 0세대에는 you라는 값이 저장된다.
가비지 컬렉터는 제너레이션에 값들이 임계값을 초과할 경우 동작해서 레퍼런스 카운트 값이 0일때 해당 값을 지워주고 나머지 값들은 이전 세대로 올리는 역할을 한다
reference
두개의 글과 동영상 정리가 잘 되어 있으니 참고해보길 !
https://www.youtube.com/watch?v=UwGHc6A0Jq8&t=1613s
https://dc7303.github.io/python/2019/08/06/python-memory/