Python 가비지 컬렉션: 알아야 할 모든 것
Jan 17, 2025
# python
Emily Parker
Product Engineer · Leapcell

I. 가비지 컬렉션 개요
- 컴퓨터 과학에서 가비지 컬렉션(GC로 약칭)은 자동 메모리 관리 메커니즘입니다. 프로그램이 점유한 특정 메모리 공간에 더 이상 액세스할 수 없으면 프로그램은 가비지 컬렉션 알고리즘을 통해 운영 체제로 반환합니다.
- 가비지 컬렉터는 프로그래머의 부담을 줄이고 프로그램 오류를 최소화할 수 있습니다. LISP 언어에서 유래되었습니다.
- 현재 Smalltalk, Java, C#, Go, D와 같은 많은 언어가 가비지 컬렉터를 지원합니다.
- 최신 프로그래밍 언어의 자동 메모리 관리 메커니즘인 GC는 주로 두 가지 작업을 수행합니다.
- 메모리에서 쓸모없는 가비지 리소스를 식별합니다.
- 이러한 가비지를 지우고 다른 객체가 활용할 수 있도록 메모리를 해제합니다.
- 프로그래머는 리소스 관리의 무거운 부담에서 벗어나 비즈니스 로직에 더 집중할 수 있습니다. 그러나 프로그래머는 여전히 GC를 이해해야 하며, 이는 더 강력한 코드를 작성하는 데 도움이 됩니다.
II. 일반적인 가비지 컬렉션 알고리즘
- 참조 횟수:
- 각 객체에 대한 참조 횟수를 유지합니다. 이 객체를 참조하는 객체가 소멸되면 참조 횟수가 1씩 감소합니다. 객체의 참조 횟수가 0에 도달하면 객체가 재활용됩니다.
- 대표적인 언어: Python, PHP, Swift.
- 장점: 빠른 객체 재활용이 가능하며 메모리가 소진되었거나 특정 임계값에 도달했을 때만 재활용하지 않습니다.
- 단점: 순환 참조를 효과적으로 처리할 수 없으며 참조 횟수를 실시간으로 유지하는 데 오버헤드가 발생합니다.
- Mark-Sweep:
- 루트 변수에서 시작하여 참조된 모든 객체를 탐색하고 참조된 객체를 표시하고 표시되지 않은 객체를 재활용합니다.
- 대표적인 언어: Golang (삼색 마킹 방법), Python (보조).
- 장점: 참조 횟수의 단점을 극복합니다.
- 단점: STW(프로그램 실행을 일시적으로 중지)가 필요합니다.
- Generational Collection:
- 객체의 수명에 따라 서로 다른 세대 공간을 나눕니다. 수명이 긴 객체는 오래된 세대에 배치되고 수명이 짧은 객체는 새로운 세대에 배치됩니다. 세대마다 재활용 알고리즘과 빈도가 다릅니다.
- 대표적인 언어: Java, Python (보조).
- 장점: 우수한 재활용 성능.
- 단점: 알고리즘이 복잡합니다.
III. Python의 가비지 컬렉션 메커니즘
- 공식 문서 설명:
- Python의 메모리 관리 세부 사항은 구현에 따라 다릅니다.
- CPython은 참조 횟수를 사용하여 액세스할 수 없는 객체를 감지하고, 참조 주기를 수집하기 위해 또 다른 메커니즘을 사용합니다. 액세스할 수 없는 주기를 찾고 관련된 객체를 삭제하기 위해 주기 감지 알고리즘을 주기적으로 실행합니다.
- gc 모듈은 가비지 컬렉션을 수행하고 디버깅 통계를 얻고 컬렉터 매개변수를 최적화하는 기능을 제공합니다.
- 다른 구현(예: Jython 또는 PyPy)은 완전한 가비지 컬렉터와 같은 다른 메커니즘에 의존할 수 있습니다. Python 코드가 참조 횟수로 구현된 동작에 의존하는 경우 이식성 문제가 발생할 수 있습니다.
- 참조 횟수:
- Python에서 채택한 기본 가비지 컬렉션 메커니즘은 참조 횟수 방법으로, 1960년 George E. Collins가 처음 제안했으며 오늘날에도 여전히 많은 프로그래밍 언어에서 사용됩니다.
- 원리: 각 객체는 객체가 현재 참조되는 횟수를 기록하는 ob_ref 필드를 유지 관리합니다. 새 참조가 객체를 가리키면 ob_ref가 1씩 증가합니다. 참조가 유효하지 않게 되면 ob_ref가 1씩 감소합니다. 참조 횟수가 0이면 객체가 즉시 재활용되고 점유된 메모리 공간이 해제됩니다.
- 단점: 참조 횟수를 유지 관리하기 위한 추가 공간이 필요하며 객체의 "순환 참조"를 해결할 수 없습니다. 예를 들어:
a = {} # 객체 A의 참조 횟수는 1입니다. b = {} # 객체 B의 참조 횟수는 1입니다. a['b'] = b # B의 참조 횟수가 1씩 증가합니다. b['a'] = a # A의 참조 횟수가 1씩 증가합니다. del a # A의 참조가 1씩 감소하고 객체 A의 최종 참조는 1입니다. del b # B의 참조가 1씩 감소하고 객체 B의 최종 참조는 1입니다.
- 위의 예에서 del 문이 실행된 후 객체 A와 B는 순환 참조를 형성합니다. 외부 참조는 없지만 참조 횟수는 0이 아니므로 재활용되지 않아 메모리 누수가 발생할 수 있습니다.
- Mark-Sweep:
- 추적 GC 기술을 기반으로 구현된 가비지 컬렉션 알고리즘으로, 두 단계로 구성됩니다.
- 마킹 단계: 모든 "활성 객체"를 표시합니다.
- 스위핑 단계: 표시되지 않은 "비활성 객체"를 재활용합니다.
- 루트 객체(예: 전역 변수, 호출 스택 및 레지스터)에서 시작하여 방향성 에지를 따라 객체를 탐색합니다. 도달 가능한 객체는 활성 객체로 표시되고 도달할 수 없는 객체는 비활성 객체로 표시된 다음 지워집니다.
- Python의 보조 가비지 컬렉션 기술인 mark-sweep 알고리즘은 주로 컨테이너 객체(예: list, dict, tuple, instance 등)를 처리합니다. 문자열 및 숫자 객체는 순환 참조 문제를 일으키지 않기 때문입니다.
- Python은 이중 연결 목록을 사용하여 이러한 컨테이너 객체를 구성합니다.
- 단점: 비활성 객체를 지우기 전에 전체 힙 메모리를 순차적으로 스캔해야 합니다. 활성 객체의 작은 부분만 남아 있더라도 모든 객체를 스캔해야 합니다.
- 추적 GC 기술을 기반으로 구현된 가비지 컬렉션 알고리즘으로, 두 단계로 구성됩니다.
- Generational Recycling:
- 공간과 시간을 교환하는 작업 모드입니다. 메모리는 객체의 생존 시간에 따라 여러 세트로 나뉘며 각 세트는 세대로 간주됩니다. Python은 young generation(세대 0), middle generation(세대 1) 및 old generation(세대 2)의 세 가지 세대로 나뉘며, 이는 세 개의 연결 목록에 해당합니다. 객체의 생존 시간이 길어질수록 가비지 컬렉션 빈도가 줄어듭니다.
- 새로 생성된 객체는 young generation에 할당됩니다. young generation의 연결 목록의 총 수가 상한에 도달하면 가비지 컬렉션 메커니즘이 트리거됩니다. 재활용 가능한 객체는 재활용되고 재활용 불가능한 객체는 middle generation으로 이동하는 식입니다. old generation의 객체는 가장 오래 생존합니다.
- Generational Recycling은 mark-sweep 기술을 기반으로 하며 Python에서 컨테이너 객체를 처리하기 위한 보조 가비지 컬렉션 기술 역할도 합니다.
IV. 메모리 누수
- 메모리 누수는 일상적인 Python 사용에서 비교적 드뭅니다.
- CPython이 종료 시 모든 메모리를 해제하지 않는 경우:
- 전역 네임스페이스 또는 Python 모듈에서 참조되는 객체가 항상 해제되는 것은 아니며 순환 참조가 있는 경우 발생할 수 있습니다. C 라이브러리에서 할당한 일부 메모리도 해제되지 않을 수 있습니다.
- Python은 종료 시 메모리를 정리하고 각 객체를 삭제하려고 시도합니다.
- 해제 시 특정 콘텐츠를 강제로 삭제하려면 atexit 모듈을 사용하여 함수를 실행할 수 있습니다.
- 코드 예시:
# 일부 Python 구현에서 다음 코드(CPython에서 잘 작동함)는 파일 설명자를 소모할 수 있습니다. for file in very_long_list_of_files: f = open(file) c = f.read(1)
- 개선된 솔루션:
# 파일을 명시적으로 닫거나 메모리 관리 체계와 관계없이 유효한 with 문을 사용해야 합니다. for file in very_long_list_of_files: with open(file) as f: c = f.read(1)
Leapcell: Python 앱 호스팅을 위한 최고의 서버리스 플랫폼
마지막으로 Python 서비스 배포에 가장 적합한 플랫폼인 Leapcell을 소개합니다.
1. 다중 언어 지원
- JavaScript, Python, Go 또는 Rust를 사용하여 개발합니다.
2. 무제한 프로젝트를 무료로 배포
- 실제 사용량에 대해서만 비용을 지불합니다. 요청이 없으면 요금이 부과되지 않습니다.
3. 타의 추종을 불허하는 비용 효율성
- 유휴 요금 없이 사용량에 따라 지불합니다.
- 예: $25로 평균 응답 시간이 60밀리초인 694만 건의 요청을 지원할 수 있습니다.
4. 간소화된 개발자 경험
- 쉬운 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
5. 간편한 확장성 및 고성능
- 고도의 동시성을 쉽게 처리할 수 있도록 자동 확장됩니다.
- 운영 오버헤드가 없어 구축에만 집중할 수 있습니다.
Leapcell 트위터: https://x.com/LeapcellHQ