GC는 모던한 언어에게는 필수적으로 탑재되어 있습니다. C#, JS를 포함하여 JVM에서 GC를 해주는 JAVA 그리고 PYTHON 까지 우리는 응용프로그램을 돌리면서 수많은 메모리를 생각없이 사용해왔고 Jquery를 사용하면서 귀찮으니 따로 변수를 설정하지 않고 $('#text_input') 을 남발해 왔습니다. GC가 원래는 없지만, 라이브러리 형태로 사용할수 있는 C, C++, Rust 같은 언어들도 있습니다. 하지만 모던 랭기지에는 더이상 개발자는 신경을 쓰지 않게 되었죠.
메모리를 직접 관리 해주면 아래와 같은 애로 사항이 있습니다.
필요없는 메모리를 비워주지 않으면, 이것이 메모리 누수로 이어지고, 이 후 치명적인 문제가 될 수 있습니다.
사용중인 메모리를 비워버리면, 프로그램은 중단되고, 일부 데이터가 손상될 수 있습니다.
Cpython에서 메모리를 관리하는 방법은 2가지가 있습니다.
Generational Garbage Collection(세대별 가비지 컬렉션)
Reference Counting (레퍼런스 카운팅)
이중 파이썬은 레퍼런스 카운팅을 주로 사용합니다.
우리가 객체를 만들때마다 얼마나 그 객체가 사용되고 있는지 카운팅 하기 시작합니다.
객체가 참조될때마다 증가되고 참조가 해제되면 감소합니다. 이것이 0이되면 메모리 할당을 릴리즈 하게 되죠.
얼마나 내가 만든 객체가 참조되고 있는지 궁금하다고요?
sys 라이브러리 안에 getrefcount함수로 가능합니다.
import sys
text = '나는 쓰레기차다!'
print(sys.getrefcount(text))
lst = [text]
print(sys.getrefcount(text))
tup = (text)
print(sys.getrefcount(text))
dic = {'text': text}
print(sys.getrefcount(text))
a = text
print(sys.getrefcount(text))
>>> 2
>>> 3
>>> 4
>>> 5
>>> 6
text 변수 지정될때 1번 (누적 1회)
첫 print할때 text가 변수로 넘어가면서 1번 (누적 2회)
list에 인용되면서 1번 (누적 3회)
다음 print 부터는 패스~
tuple에 인용되면서 1번 (누적 4회)
dict에 인용되면서 1번 (누적 5회)
다른 변수에 인용되면서 1번 (누적 6회)
또다른 예,
import sys
liAbc = ['a', 'b', 'c']
print(sys.getrefcount('a')) # 8
print(sys.getrefcount('b')) # 11
print(sys.getrefcount('c')) # 16
del liAbc
print(sys.getrefcount('a')) # 7
print(sys.getrefcount('b')) # 10
print(sys.getrefcount('c')) # 15
CPython은 사이드로 세대별 가비지 컬렉션이라는 기능도 있는데, 주된 방법은 아닙니다.
이것은 참조는 되어 있지만, 접근할 수 없는 객체를 메모리에서 릴리즈하는 역할을 한다. 순환참조(Circular References) 에서 나타날 수 있는데..
유용한 예가 있습니다.
l = []
l.append(l)
del l
객체 스스로가 스스로를 리스트에 넣고 스스로를 지웠습니다.
이런경우에는 참조는 되어 있지만, 해당 객체에 접근할 수 없고, 이때 등판하는 투수가 세대별 가비지 컬렉터입니다.
이 GC 모듈은 이것만을 잡아내고 이것만을 해결하는 역할을 하고, 나머지는 레퍼런스 카운팅이 다 처리합니다.
PEP8 가이드 라인에 따르면,
"당신이 정말 순환참조를 만들지 않을 자신이 있다면, gc.disable() 로 GC의 작동을 멈출 수 있다. "
라고 되어 있습니다.
순환참조를 어떻게 찾아낼까?
우선 순환참조는 컨테이너 객체에서만 생길 수 있습니다. 컨테이너 객체는 Tuple, List, Set, Dict, Class 같은 경우들 이지요. 이 컨테이너 객체들과 서로서로 참조하는 객체들만 다 따라 다니면서 잡아내기 시작합니다. 그리고 접근할 수 없는 객체에 대해서 메모리에서 해제 합니다.
import gc
print(gc.get_count())
print(gc.get_threshold())
>>> (293, 5, 1)
>>> (700, 10, 10)
위에 수치는 현재 나의 객체수다. 어린세대 293개, 중간세대 5개, 늙은세대 1개.
그리고 아래 수치는 GC를 수행하는 임계점입니다. 각 세대별로 저 수치에 오면 GC를 수행한다. 물론, gc.collect() 로 수동 가비지 콜렉션도 가능합니다. 해당 임계점을 높이고 싶으면 아래 명령어로 수행이 가능합니다.
import gc
gc.set_threshold(800, 13, 10)
print(gc.get_threshold())
>>> (800, 13, 10)
쓰레기를 실어 옮길려면 GC는 프로세스에게 멈추라고 지시합니다. 이 임계점이 높으면 높을 수록 덜 자주 멈추지만, 낮으면 낮을 수록 멈추는 시간이 적고 메모리 사용량이 적어집니다. 아까 위에 언급했던 PEP8 에서 "아니 왜.. 대체 GC를 멈추는 겁니까?" 라고 하지만 임계점을 0으로 바꾸고 GC를 멈춘 후 DJANGO 를 사용하는 인스타그램의 퍼포먼스가 10% 상승한 예가 있습니다.