Skip to content

Python Memory Model — Tham chiếu, đối tượng và garbage collection

Một microservice Python chạy ổn định suốt hai tuần. Tuần thứ ba, memory tăng đều đặn, 100MB mỗi ngày. Tuần thứ tư, OOM killer hạ service lúc 3 giờ sáng. Bạn debug và phát hiện: một dict cache không có size limit, cộng với một circular reference giữa hai class khiến garbage collector không thể dọn dẹp. Hai bug kinh điển mà bất kỳ ai viết Python backend đều sẽ gặp — nếu không hiểu memory model.

Trong Python, mọi thứ đều là object — số 42, string "hello", function print, kể cả class int bản thân nó. Biến không phải hộp chứa giá trị — biến là nhãn dán (name tag) gắn vào object. Hiểu điều này thay đổi căn bản cách bạn nghĩ về phép gán, truyền tham số, và quản lý bộ nhớ.

Bài này đi từ mô hình tham chiếu, qua is vs ==, mutable vs immutable, đến reference counting và generational GC. Cuối bài là các kỹ thuật phát hiện và fix memory leaks trong production.


Bức tranh tư duy

Hãy nghĩ về bộ nhớ Python như một bãi đỗ xe lớn.

Mỗi object là một chiếc xe đậu trong bãi. Mỗi biến là một tấm biển chỉ đường trỏ đến vị trí xe. Khi bạn viết a = [1, 2, 3], bạn đậu một chiếc xe (list object) và cắm biển a trỏ đến nó. Viết b = a không tạo xe mới — nó cắm thêm biển b trỏ đến cùng chiếc xe.

Reference counting là hệ thống đếm xem mỗi xe có bao nhiêu biển chỉ đường. Khi không còn biển nào trỏ đến, xe được kéo đi lập tức (deallocate). Nhưng nếu hai xe có dây xích buộc vào nhau (circular reference) mà không có biển nào trỏ đến, hệ thống đếm biển không dọn được. Lúc này, garbage collector (GC) là đội tuần tra định kỳ dọn dẹp những cụm xe bỏ bao vây.

is kiểm tra xem hai biển có trỏ đến cùng một chiếc xe không (identity). == kiểm tra xem hai chiếc xe có cùng màu sơn và biển số không (equality). Hai xe khác nhau có thể giống hệt nhau (== True) nhưng vẫn là hai xe (is False).


Cốt lõi kỹ thuật

Mọi thứ đều là object

python
import sys

# Số, string, function, class — tất cả đều là object
print(type(42))         # <class 'int'>
print(type("hello"))    # <class 'str'>
print(type(print))      # <class 'builtin_function_or_method'>
print(type(int))        # <class 'type'>

# Mọi object đều có id (vị trí trong bộ nhớ), type, và value
x = [1, 2, 3]
print(id(x))     # địa chỉ bộ nhớ, ví dụ: 140234567890
print(type(x))   # <class 'list'>
print(x)         # [1, 2, 3]

Biến là tham chiếu, không phải hộp chứa

python
a = [1, 2, 3]
b = a  # b trỏ đến CÙNG object với a

b.append(4)
print(a)  # [1, 2, 3, 4] — a cũng bị thay đổi!
print(a is b)  # True — cùng một object

# Để tạo bản sao độc lập:
c = a.copy()        # shallow copy
c.append(5)
print(a)  # [1, 2, 3, 4] — a không bị ảnh hưởng
print(a is c)  # False — khác object

Mutable vs Immutable

KiểuMutable?Ví dụ
int, float, boolImmutable42, 3.14, True
str, bytesImmutable"hello", b"data"
tuple, frozensetImmutable(1, 2), frozenset({1})
listMutable[1, 2, 3]
dictMutable{"a": 1}
setMutable{1, 2, 3}

Immutable objects không thể thay đổi sau khi tạo. Khi bạn "thay đổi" một string, thực tế Python tạo một object mới:

python
s = "hello"
print(id(s))  # 140234567890

s += " world"
print(id(s))  # 140234567891 — OBJECT MỚI, không phải sửa object cũ

is vs == — identity vs equality

python
# == kiểm tra VALUE (gọi __eq__)
# is kiểm tra IDENTITY (cùng object trong bộ nhớ)

a = [1, 2, 3]
b = [1, 2, 3]
c = a

print(a == b)  # True — cùng giá trị
print(a is b)  # False — khác object
print(a is c)  # True — cùng object

# Khi nào dùng `is`:
# 1. So sánh với None
if value is None:
    ...

# 2. So sánh với sentinel values
_MISSING = object()
def get(key: str, default: object = _MISSING) -> object:
    result = cache.get(key, _MISSING)
    if result is _MISSING:
        raise KeyError(key)
    return result

# KHÔNG dùng `is` cho giá trị thường:
if x is 0:       # SAI — không đảm bảo
if x is "hello":  # SAI — phụ thuộc implementation

Reference counting

python
import sys

a = [1, 2, 3]
print(sys.getrefcount(a))  # 2 (a + tham số của getrefcount)

b = a                       # +1 reference
print(sys.getrefcount(a))  # 3

my_list = [a]               # +1 reference
print(sys.getrefcount(a))  # 4

del b                        # -1 reference
print(sys.getrefcount(a))  # 3

my_list.clear()              # -1 reference
print(sys.getrefcount(a))  # 2

# Khi refcount về 0, CPython deallocate ngay lập tức
del a  # object [1, 2, 3] được giải phóng

Generational Garbage Collection

Reference counting không xử lý được circular references. CPython dùng generational GC bổ sung với 3 thế hệ:

GenerationChứaTần suất dọn
Gen 0Objects mới tạoThường xuyên nhất
Gen 1Sống sót qua Gen 0Ít hơn
Gen 2Sống sót qua Gen 1Hiếm nhất

Giả thuyết cơ bản: hầu hết objects chết trẻ (die young). Objects sống sót qua nhiều lần dọn có xu hướng sống lâu — nên không cần kiểm tra thường xuyên.

python
import gc

# Xem thresholds
print(gc.get_threshold())  # (700, 10, 10)
# Gen 0: dọn sau 700 allocations
# Gen 1: dọn sau 10 lần Gen 0
# Gen 2: dọn sau 10 lần Gen 1

# Tùy chỉnh (thường không cần)
gc.set_threshold(1000, 15, 15)

# Force collection
collected = gc.collect()
print(f"Dọn được {collected} unreachable objects")

# Xem thống kê
for i, stats in enumerate(gc.get_stats()):
    print(f"Gen {i}: {stats['collections']} collections, "
          f"{stats['collected']} collected")

Weak references

Weak reference trỏ đến object nhưng không tăng refcount. Khi object bị deallocate, weak reference tự động trả về None.

python
import weakref

class ExpensiveResource:
    def __init__(self, name: str) -> None:
        self.name = name

obj = ExpensiveResource("database-pool")
weak = weakref.ref(obj)

print(weak())        # <ExpensiveResource object ...>
print(weak().name)   # "database-pool"

del obj
print(weak())        # None — object đã bị deallocate

# WeakValueDictionary — cache tự dọn dẹp
cache: weakref.WeakValueDictionary[str, ExpensiveResource] = (
    weakref.WeakValueDictionary()
)

resource = ExpensiveResource("conn-pool")
cache["pool"] = resource

print("pool" in cache)  # True
del resource
print("pool" in cache)  # False — tự động xóa

__slots__ — giảm memory footprint

python
import sys

# Class thường: mỗi instance có __dict__ (56+ bytes overhead)
class UserNormal:
    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age

# Class với __slots__: không có __dict__
class UserSlotted:
    __slots__ = ("name", "age")

    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age

normal = UserNormal("Alice", 30)
slotted = UserSlotted("Alice", 30)

print(sys.getsizeof(normal) + sys.getsizeof(normal.__dict__))
# ~ 200 bytes

print(sys.getsizeof(slotted))
# ~ 56 bytes — tiết kiệm đáng kể khi có hàng triệu instances

# Nhược điểm: không thể thêm attribute động
# slotted.email = "a@b.com"  # AttributeError!

Thực chiến

Tình huống 1: Phát hiện memory leak trong long-running service

python
import tracemalloc
import gc

def diagnose_memory() -> None:
    """Chạy định kỳ trong production để track memory."""
    tracemalloc.start()

    # ... chạy business logic ...

    snapshot = tracemalloc.take_snapshot()
    top_stats = snapshot.statistics('lineno')

    print("=== Top 10 memory consumers ===" )
    for stat in top_stats[:10]:
        print(stat)

    current, peak = tracemalloc.get_traced_memory()
    print(f"Current: {current / 1024 / 1024:.1f} MB")
    print(f"Peak: {peak / 1024 / 1024:.1f} MB")

    tracemalloc.stop()

# So sánh hai snapshots để tìm leak
def compare_snapshots() -> None:
    tracemalloc.start()
    snapshot1 = tracemalloc.take_snapshot()

    # ... chạy code nghi ngờ leak ...

    snapshot2 = tracemalloc.take_snapshot()
    diff = snapshot2.compare_to(snapshot1, 'lineno')

    print("=== Memory diff (top 10 increases) ===")
    for stat in diff[:10]:
        print(stat)

Tình huống 2: Fix circular reference trong tree structure

python
import weakref
from typing import Self

class TreeNode:
    """Tree node với weak reference đến parent để tránh circular ref."""
    __slots__ = ("value", "_parent", "children")

    def __init__(self, value: str) -> None:
        self.value = value
        self._parent: weakref.ref[Self] | None = None
        self.children: list[Self] = []

    @property
    def parent(self) -> "TreeNode | None":
        if self._parent is None:
            return None
        return self._parent()  # dereference weak ref

    def add_child(self, child: "TreeNode") -> None:
        child._parent = weakref.ref(self)  # weak ref, không tăng refcount
        self.children.append(child)

# Sử dụng
root = TreeNode("root")
child1 = TreeNode("child1")
child2 = TreeNode("child2")
root.add_child(child1)
root.add_child(child2)

print(child1.parent.value)  # "root"
del root
# root được deallocate ngay, không bị giữ bởi circular reference

Sai lầm điển hình

Sai lầm 1: Dùng is thay vì == cho giá trị

python
# SAI: Phụ thuộc vào integer caching của CPython
x = 256
y = 256
print(x is y)  # True — nhưng chỉ vì CPython cache -5 đến 256

x = 257
y = 257
print(x is y)  # Có thể True hoặc False! Phụ thuộc context.

# ĐÚNG: Luôn dùng == cho so sánh giá trị
print(x == y)  # True — luôn đúng

# Chỉ dùng `is` cho: None, True, False, sentinel objects

Sai lầm 2: Nhầm shallow copy với deep copy

python
import copy

# SAI: Nghĩ rằng copy() tạo bản sao hoàn toàn độc lập
original = [[1, 2], [3, 4]]
shallow = original.copy()  # hoặc list(original) hoặc original[:]

shallow[0].append(999)
print(original)  # [[1, 2, 999], [3, 4]] — BUG! nested list bị ảnh hưởng

# Shallow copy chỉ copy container ngoài cùng,
# các nested objects vẫn là REFERENCE chung.

# ĐÚNG: Deep copy cho nested structures
deep = copy.deepcopy(original)
deep[0].append(888)
print(original)  # [[1, 2, 999], [3, 4]] — không bị ảnh hưởng

Sai lầm 3: Memory leak do unbounded cache

python
# SAI: Cache không giới hạn size
user_cache: dict[int, dict] = {}

def get_user(user_id: int) -> dict:
    if user_id not in user_cache:
        user_cache[user_id] = fetch_from_db(user_id)
    return user_cache[user_id]
# Sau vài tháng, user_cache chứa hàng triệu entries

# ĐÚNG CÁCH 1: LRU cache với size limit
from functools import lru_cache

@lru_cache(maxsize=10_000)
def get_user(user_id: int) -> dict:
    return fetch_from_db(user_id)

# ĐÚNG CÁCH 2: TTL cache (cần cài thư viện hoặc tự implement)
# xem bài tập ở phần cuối

# ĐÚNG CÁCH 3: WeakValueDictionary
from weakref import WeakValueDictionary
user_cache: WeakValueDictionary[int, UserObject] = WeakValueDictionary()

Sai lầm 4: Dựa vào __del__ cho cleanup

python
# SAI: __del__ không đảm bảo được gọi (circular refs, interpreter shutdown)
class DatabaseConnection:
    def __init__(self) -> None:
        self.conn = create_connection()

    def __del__(self) -> None:
        self.conn.close()  # có thể không được gọi!

# ĐÚNG: Context manager
class DatabaseConnection:
    def __init__(self) -> None:
        self.conn = create_connection()

    def __enter__(self) -> "DatabaseConnection":
        return self

    def __exit__(self, *args: object) -> None:
        self.conn.close()  # đảm bảo được gọi

with DatabaseConnection() as db:
    db.conn.execute("SELECT 1")
# conn.close() được gọi chắc chắn khi thoát `with`

Sai lầm 5: Closure giữ reference đến large object

python
# SAI: Closure giữ toàn bộ dataset trong bộ nhớ
def create_filter(dataset: list[dict]) -> Callable[[str], list[dict]]:
    def filter_by_status(status: str) -> list[dict]:
        return [d for d in dataset if d["status"] == status]  # dataset: 1GB
    return filter_by_status

big_data = load_huge_dataset()  # 1GB
my_filter = create_filter(big_data)
del big_data  # KHÔNG giải phóng! closure vẫn giữ reference

# ĐÚNG: Chỉ giữ dữ liệu cần thiết
def create_filter(dataset: list[dict]) -> Callable[[str], list[dict]]:
    # Index trước, chỉ giữ những gì cần
    by_status: dict[str, list[dict]] = {}
    for item in dataset:
        by_status.setdefault(item["status"], []).append(item)

    def filter_by_status(status: str) -> list[dict]:
        return by_status.get(status, [])

    return filter_by_status

Under the Hood

CPython object header

Mọi object trong CPython bắt đầu bằng một struct PyObject chứa ít nhất hai trường:

+------------------+
| ob_refcnt (8B)   |  <-- số references trỏ đến object
+------------------+
| ob_type   (8B)   |  <-- con trỏ đến type object (PyTypeObject*)
+------------------+
| ...data...       |  <-- dữ liệu riêng của từng type
+------------------+

Với container types (list, dict), có thêm ob_size cho biết số phần tử:

+------------------+
| ob_refcnt (8B)   |
+------------------+
| ob_type   (8B)   |
+------------------+
| ob_size   (8B)   |  <-- số phần tử (PyVarObject)
+------------------+
| ...items...      |
+------------------+

Mỗi khi bạn gán b = a, CPython chỉ tăng ob_refcnt của object lên 1 — không copy dữ liệu.

Small integer caching

CPython pre-allocate và cache các số nguyên từ -5 đến 256 khi khởi động. Mọi biến trỏ đến cùng giá trị trong khoảng này sẽ trỏ đến cùng object.

python
a = 100
b = 100
print(a is b)  # True — cùng cached object

a = 300
b = 300
print(a is b)  # Có thể True hoặc False, phụ thuộc cách CPython optimize

# Không bao giờ dựa vào hành vi này để viết logic!

Khoảng cache này được định nghĩa trong source code CPython (Objects/longobject.c), có thể khác ở các Python implementation khác (PyPy, Jython).

String interning

CPython tự động intern một số strings (identifier-like strings, tức là strings chỉ chứa chữ cái, số, và underscore):

python
a = "hello"
b = "hello"
print(a is b)  # True — interned

a = "hello world"
b = "hello world"
print(a is b)  # Có thể True hoặc False

# Force interning
import sys
a = sys.intern("hello world")
b = sys.intern("hello world")
print(a is b)  # True — chắc chắn cùng object

# Hữu ích khi có rất nhiều strings giống nhau (đọc từ file, database)
# Giảm memory và tăng tốc so sánh (is nhanh hơn ==)

gc module internals

python
import gc

# Debug circular references
gc.set_debug(gc.DEBUG_SAVEALL)  # lưu unreachable objects vào gc.garbage

class A:
    pass

class B:
    pass

a = A()
b = B()
a.ref = b
b.ref = a
del a, b

gc.collect()
print(gc.garbage)  # [<A object>, <B object>] — circular refs đã dọn

# Track referrers và referents
obj = [1, 2, 3]
container = {"data": obj}

print(gc.get_referrers(obj))   # [{'data': [1, 2, 3]}, ...] — ai trỏ đến obj
print(gc.get_referents(container))  # [[1, 2, 3]] — container trỏ đến gì

# Kiểm tra object có được GC track không
print(gc.is_tracked(obj))    # True — container types được track
print(gc.is_tracked(42))     # False — immutable atoms không được track

Memory layout so sánh

python
import sys

# Chi phí bộ nhớ của các container rỗng
print(f"int(0):        {sys.getsizeof(0)} bytes")        # 28
print(f"float(0.0):    {sys.getsizeof(0.0)} bytes")      # 24
print(f"str(''):       {sys.getsizeof('')} bytes")        # 49
print(f"list([]):      {sys.getsizeof([])} bytes")        # 56
print(f"dict({{}}):      {sys.getsizeof(dict())} bytes")   # 64
print(f"set():         {sys.getsizeof(set())} bytes")     # 216
print(f"tuple(()):     {sys.getsizeof(())} bytes")        # 40

# Lưu ý: getsizeof chỉ tính container, không tính elements bên trong

Checklist ghi nhớ

✅ Checklist triển khai

Tham chiếu và so sánh

  • [ ] Biến là tham chiếu, không phải hộp chứa — gán b = a không copy
  • [ ] Dùng == cho so sánh giá trị, is chỉ cho None và sentinels
  • [ ] Không dựa vào integer caching (-5 đến 256) hay string interning

Copy

  • [ ] copy() là shallow — nested objects vẫn shared
  • [ ] copy.deepcopy() cho nested structures cần độc lập hoàn toàn

Memory management

  • [ ] Dùng __slots__ cho classes có nhiều instances
  • [ ] Dùng weakref cho parent references và caches
  • [ ] Dùng context managers thay vì __del__ cho resource cleanup
  • [ ] Giới hạn size của caches (lru_cache, deque(maxlen=))

Debugging và profiling

  • [ ] sys.getrefcount() để kiểm tra reference count
  • [ ] tracemalloc để tìm memory consumers
  • [ ] gc.get_referrers() để trace ai giữ reference đến object
  • [ ] gc.collect() để force garbage collection khi debug

Bài tập luyện tập

Bài 1: Reference counting explorer (Foundation)

Viết một function nhận vào một object và in ra reference count ở từng bước: tạo biến mới, thêm vào list, xóa biến, xóa list. Giải thích tại sao refcount thay đổi ở mỗi bước.

Gợi ý
python
import sys

def explore_refcount() -> None:
    a = [1, 2, 3]
    print(f"Sau khi tạo a: {sys.getrefcount(a)}")  # 2 (a + getrefcount param)

    b = a
    print(f"Sau b = a: {sys.getrefcount(a)}")      # 3

    container = [a]
    print(f"Sau [a]: {sys.getrefcount(a)}")         # 4

    del b
    print(f"Sau del b: {sys.getrefcount(a)}")       # 3

    container.clear()
    print(f"Sau clear(): {sys.getrefcount(a)}")     # 2

explore_refcount()

Bài 2: Memory leak detector (Intermediate)

Viết một context manager MemoryTracker sử dụng tracemalloc để đo memory usage của một block code. In ra current memory, peak memory, và top 5 consumers khi thoát block.

Gợi ý
python
import tracemalloc
from types import TracebackType

class MemoryTracker:
    def __enter__(self) -> "MemoryTracker":
        tracemalloc.start()
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: TracebackType | None,
    ) -> None:
        snapshot = tracemalloc.take_snapshot()
        current, peak = tracemalloc.get_traced_memory()
        tracemalloc.stop()

        print(f"Current: {current / 1024:.1f} KB")
        print(f"Peak:    {peak / 1024:.1f} KB")
        print("Top 5 consumers:")
        for stat in snapshot.statistics('lineno')[:5]:
            print(f"  {stat}")

with MemoryTracker():
    data = [i ** 2 for i in range(100_000)]

Bài 3: Circular reference detector (Advanced)

Viết function find_circular_refs(obj) dùng gc.get_referents() để phát hiện circular references bắt đầu từ một object. Trả về list các cycles tìm được.

Gợi ý

Sử dụng DFS với visited set và recursion stack để phát hiện cycles. Lưu ý: đây là bài tập nâng cao, cần xử lý cẩn thận với các container types.

python
import gc

def find_circular_refs(root: object) -> list[list[object]]:
    cycles: list[list[object]] = []
    visited: set[int] = set()
    path: list[object] = []

    def dfs(obj: object) -> None:
        obj_id = id(obj)
        if obj_id in visited:
            # Tìm vị trí bắt đầu cycle
            for i, item in enumerate(path):
                if id(item) == obj_id:
                    cycles.append(path[i:])
                    return
            return

        visited.add(obj_id)
        path.append(obj)

        for ref in gc.get_referents(obj):
            if not isinstance(ref, type):  # bỏ qua type objects
                dfs(ref)

        path.pop()

    dfs(root)
    return cycles

🧠 Quiz

sys.getrefcount(x) trả về 2 khi gọi với biến x duy nhất. Tại sao không phải 1?

  • [ ] Vì Python luôn giữ 1 reference ẩn
  • [x] Vì tham số của getrefcount() tạo thêm 1 reference tạm
  • [ ] Vì GC giữ 1 reference
  • [ ] Vì biến local và global tính riêng

Giải thích: Khi gọi sys.getrefcount(x), giá trị của x được truyền vào function như tham số, tạo thêm một reference tạm thời. Vì vậy kết quả luôn lớn hơn thực tế ít nhất 1.

🧠 Quiz

Object nào sau đây KHONG được garbage collector track?

  • [ ] [1, 2, 3]
  • [ ] {"a": 1}
  • [x] 42
  • [ ] {1, 2, 3}

Giải thích: GC chỉ track container types (list, dict, set, instances) vì chỉ chúng mới có thể tạo circular references. Immutable atoms như int, str, float không được track.


Liên kết học tiếp

Từ khóa: reference counting, garbage collection, generational GC, ob_refcnt, ob_type, PyObject, is vs ==, mutable, immutable, shallow copy, deep copy, weakref, __slots__, tracemalloc, memory leak, circular reference, string interning, integer caching