Python과 메모리 관리, 파트 2: Heap Memory

발행: (2026년 2월 8일 오전 06:08 GMT+9)
12 분 소요
원문: Dev.to

I’m happy to translate the article for you, but I’ll need the full text you’d like translated. Could you please paste the content (excluding the source line you already provided) here? Once I have the text, I’ll translate it into Korean while preserving the original formatting, markdown syntax, and technical terms.

Source:

Python의 메모리 섹션: 객체가 위치하는 곳

Python 프로그램을 실행하면 운영 체제가 프로세스를 위해 여러 메모리 영역을 생성합니다.
Linux에서는 아래 스크립트를 사용해 해당 영역을 확인할 수 있습니다(파일에 저장하고 실행).

#!/usr/bin/env python3
import os, sys

def main():
    pid = os.getpid()
    with open(f"/proc/{pid}/maps") as f:
        for line in f:
            line = line.strip()
            parts = line.split()
            if len(parts) >>>> Found: {name} ({id(obj):#x}) in this block")

# Objects to track
vars = {
    "True": True,
    "small number": 4,
    "large number": 2**123,
    "small string": "a",
    "large string": "a" * 2000,
    "int class": int,
    "sys module": sys,
    "main function": main,
}

if __name__ == "__main__":
    main()

이 스크립트는 /proc/<pid>/maps를 읽어 각 메모리 블록을 출력하고, vars에 있는 객체 중 어느 것이 해당 블록에 존재하는지 보고합니다.

아래는 가장 흔히 보게 되는 메모리 영역에 대한 간략한 개요입니다.

1. 실행‑코드 영역 (읽기‑전용)

이 블록들은 Python 인터프리터 바이너리와 로드된 공유 라이브러리를 포함합니다.
일반적인 권한은 r—p(읽기‑전용) 또는 r‑xp(읽기‑실행)입니다.

00400000-0041f000 r--p 00000000 08:11 545993 /usr/bin/python3.14
0041f000-006d2000 r-xp 0001f000 08:11 545993 /usr/bin/python3.14
006d2000-00945000 r--p 002d2000 08:11 545993 /usr/bin/python3.14
...
7f3d6c6ab000-7f3d6c6d1000 r--p 00000000 08:11 541316 /usr/lib/x86_64-linux-gnu/libc.so.6
7f3d6c6d1000-7f3d6c827000 r-xp 00026000 08:11 541316 /usr/lib/x86_64-linux-gnu/libc.so.6

이 섹션들은 Python 바이너리 혹은 그가 의존하는 공유 객체의 일부입니다.

2. 데이터 세그먼트 (읽기‑쓰기)

이 영역은 Python 실행 파일 자체에 포함된 읽기‑쓰기 데이터를 보관합니다. 여기에는 싱글톤 객체 True, 작은 정수, 인터프리터 시작 시 미리 할당되는 내장 타입 등이 포함됩니다.

00946000-00a85000 rw-p 00545000 08:11 545993 /usr/bin/python3.14
  >>>>> Found: True (0x95a0a0) in this block
  >>>>> Found: small number (0xa5bb08) in this block
  >>>>> Found: int class (0x958cc0) in this block

3. 힙 영역 (동적 할당)

OS가 관리하는 힙(brk/sbrk)은 512 바이트를 초과하는 객체에 사용됩니다.
이 할당은 C 라이브러리의 malloc을 통해 이루어집니다.

1039c000-10449000 rw-p 00000000 00:00 0  [heap]
  >>>>> Found: large string (0x10406840) in this block

4. 익명 메모리 매핑 (Python의 pymalloc)

Python의 전용 할당자(pymalloc)는 arena라 불리는 큰 익명 메모리 블록을 확보합니다.
각 arena는 작은 객체를 위한 pool로 세분화됩니다.

>>>> Found: small string (0x7f75e62ec730) in this block
>>>> Found: main function (0x7f75e62d85e0) in this block
>>>> Found: large number (0x7f75e67e3f30) in this block
>>>> Found: sys module (0x7f75e67a2ca0) in this block

arena, pool, 전체 전략에 대한 자세한 내용은 다음을 참고하세요:

5. 스택 영역 (함수 호출)

스택은 C‑레벨 호출 프레임과 로컬 변수를 보관합니다. Python 객체 자체가 스택에 존재하지는 않습니다.

7fff20372000-7fff20394000 rw-p 00000000 00:00 0  [stack]

6. 커널‑인터페이스 영역

이 매핑([vvar], [vdso])은 커널이 빠른 시스템 콜 인터페이스를 제공하기 위해 사용합니다.
Python 객체 할당과는 직접적인 관련이 없습니다.

7fff203b8000-7fff203bc000 r--p 00000000 00:00 0  [vvar]
7fff203bc000-7fff203be000 r-xp 00000000 00:00 0  [vdso]

파이썬이 레퍼런스 카운팅으로 가비지 컬렉션을 관리하는 방법

모든 CPython 객체는 레퍼런스 카운트(ob_refcnt)를 가지고 있습니다. 카운트가 0이 되면 해당 객체의 메모리가 회수됩니다.

/* CPython’s PyObject structure */
typedef struct _object {
    Py_ssize_t ob_refcnt;    /* reference count */
    PyTypeObject *ob_type;  /* pointer to type object */
    /* ... type‑specific fields follow ... */
} PyObject;

작은 데모

import sys
import weakref

class Node:
    def __init__(self, name):
        self.name = name
        self.child = None

    def __repr__(self):
        return f"Node({self.name!r})"

# Create two nodes
a = Node('a')          # refcnt = 1
b = Node('b')          # refcnt = 1

# A weak reference does **not** increase the refcount
a_weak = weakref.ref(a)

print('a refcnt:', sys.getrefcount(a))   # usually 2 (one from getrefcount's argument)
print('a weakref alive?', a_weak() is not None)

# Create a reference cycle
a.child = b
b.child = a

print('a refcnt after cycle:', sys.getrefcount(a))
print('b refcnt after cycle:', sys.getrefcount(b))

# Break the strong references
del a
del b

# The objects are still alive because the cycle keeps their refcnt > 0,
# but the garbage collector will eventually collect them.
import gc
gc.collect()
  • sys.getrefcount()는 현재 레퍼런스 카운트를 보여줍니다 (호출 자체가 일시적인 레퍼런스를 추가합니다).
  • Weak reference(weakref.ref)는 객체를 레퍼런스 카운트에 영향을 주지 않고 참조할 수 있게 합니다.
  • 레퍼런스 사이클이 생성될 때(a.child = b; b.child = a), 레퍼런스 카운트가 0이 되지 않으므로 CPython의 순환 가비지 컬렉터가 개입해 사이클을 끊고 메모리를 해제합니다.

다음 섹션에서는 그 순환 가비지 컬렉터의 세부 사항을 살펴보겠습니다.

Source:

레퍼런스 카운팅과 약한 참조

import sys, weakref, gc

class Node:
    child = None

a = Node()
b = Node()
b_ref = weakref.ref(b)

# 사이클 생성
a.child = b
b.child = a          # 레퍼런스 카운트 = 2

print("All refs a, b, and a_ref assigned", f"{sys.getrefcount(a_ref())=}")  
# 2 (두 개의 강한 레퍼런스) + 1은 getrefcount 인수용 = 3

del a
print("After variable a deleted", f"{sys.getrefcount(a_ref())=}")   # 레퍼런스 카운트 = 1

del b
print("After both variables deleted", f"{sys.getrefcount(a_ref())=}")  
# 레퍼런스 카운트 = 0, 객체 해제 – 하지만 `sys.getrefcount`는 여전히 1을 보여줍니다!
print("After both variables deleted", f"{sys.getrefcount(b_ref())=}")  
# 같은 결과

무슨 일이 일어나고 있나요?

ab를 삭제한 뒤에도 약한 참조가 여전히 객체를 가져올 수 있습니다.
다음 코드를 추가하면 a를 아직 참조하고 있는 것이 무엇인지 확인할 수 있습니다:

import gc   # 가비지 컬렉터 인터페이스 모듈
print(f"Referrers to a: {gc.get_referrers(a_ref())}")

출력은 두 개의 레퍼런스를 보여줍니다:

  • a.child → b
  • b.child → a

변수 a를 삭제했지만 a.child 속성은 여전히 존재하므로 b를 살아 있게 하고, 반대로도 마찬가지입니다.

파이썬은 이런 사이클을 어떻게 처리하나요?

파이썬은 마크‑앤‑스윕 가비지 컬렉터를 실행합니다. 이 컬렉터는 다음과 같이 동작합니다:

  1. 루트 객체 집합(전역 변수, 로컬 변수 등)에서 시작합니다.
  2. 그 루트들로부터 도달 가능한 모든 객체를 재귀적으로 마크합니다.
  3. 마크되지 않은 객체(즉, 도달 불가능한 객체)를 스윕하여 메모리를 회수합니다.

매 할당마다 이 알고리즘을 실행하면 비용이 너무 크므로, CPython은 주기적으로 세대(generation) 임계값에 따라 실행합니다.

import gc
print(f"GC generations thresholds: {gc.get_threshold()}")   # 기본값: (700, 10, 10)

세대

세대설명컬렉션 빈도
0아직 스캔되지 않은 새로 생성된 객체700 할당마다 (기본값)
1세대 0의 컬렉션을 한 번이라도 살아남은 객체세대 0이 10번 컬렉션될 때마다
2여러 번 컬렉션을 살아남은 객체세대 1이 10번 컬렉션될 때마다

이는 세대 가설에 기반합니다: 대부분의 객체는 짧은 시간 안에 사라지고, 오래 살아남은 객체는 계속 살아 있는 경향이 있습니다.

a ↔ b 사이클이 아직 세대 0에 머물러 있기 때문에 변수들을 삭제해도 컬렉터가 아직 실행되지 않아 약한 참조가 객체에 접근할 수 있습니다.

컬렉션 강제 실행

import sys, weakref, gc

class Node:
    child = None

a = Node()
b = Node()
b_ref = weakref.ref(b)

a.child = b
b.child = a

print("All refs a, b, and a_ref assigned", f"{sys.getrefcount(a_ref())=}")

del a
print("After variable a deleted", f"{sys.getrefcount(a_ref())=}")

del b
print("After both variables deleted", f"{sys.getrefcount(b_ref())=}")

# 세대 0, 1, 2를 모두 수집하도록 강제
n_collected = gc.collect(2)
print(f"Collected {n_collected} objects")   # → 2 objects

GIL과 가비지 컬렉션의 관계

전역 인터프리터 락(Global Interpreter Lock, GIL) 은 레퍼런스 카운트 업데이트를 원자적으로 만들어 스레드 안전한 가비지 컬렉션을 보장합니다. GIL이 없으면 동시에 레퍼런스 카운트를 수정하면서 경쟁 상태가 발생하고, 메모리 손상 및 컬렉터 오류가 일어날 수 있습니다.


CPython 메모리 관리에 대한 깊이 있는 탐구를 즐기셨길 바랍니다. 레퍼런스 카운팅, 약한 참조, 그리고 세대 가비지 컬렉터를 이해하면 파이썬 코드를 작성하면서 마주치는 미묘한 동작들을 설명하는 데 큰 도움이 됩니다.

0 조회
Back to Blog

관련 글

더 보기 »

Python과 메모리 관리, 파트 1: 객체

Python의 객체 모델 이해하기: ==, is, 그리고 id 들어본 적이 있을 겁니다: Python에서는 모든 것이 객체입니다—정수, 문자열, 심지어 여러분이 작성하는 함수까지....

Python–TypeScript 계약

The Python–TypeScript Contract 표지 이미지 https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to...