Giao diện
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 objectMutable vs Immutable
| Kiểu | Mutable? | Ví dụ |
|---|---|---|
int, float, bool | Immutable | 42, 3.14, True |
str, bytes | Immutable | "hello", b"data" |
tuple, frozenset | Immutable | (1, 2), frozenset({1}) |
list | Mutable | [1, 2, 3] |
dict | Mutable | {"a": 1} |
set | Mutable | {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 implementationReference 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óngGenerational Garbage Collection
Reference counting không xử lý được circular references. CPython dùng generational GC bổ sung với 3 thế hệ:
| Generation | Chứa | Tần suất dọn |
|---|---|---|
| Gen 0 | Objects mới tạo | Thường xuyên nhất |
| Gen 1 | Sống sót qua Gen 0 | Ít hơn |
| Gen 2 | Sống sót qua Gen 1 | Hiế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 referenceSai 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 objectsSai 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ưởngSai 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_statusUnder 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 trackMemory 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 trongChecklist 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 = akhông copy - [ ] Dùng
==cho so sánh giá trị,ischỉ choNonevà 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
weakrefcho 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