引用计数:Python 的主回收机制

每个对象都有"被引用次数"——计数到 0 立即销毁:

import sys

x = [1, 2, 3]
print(sys.getrefcount(x))    # 2(包括 getrefcount 自己的临时引用)

y = x                         # 多一个引用
print(sys.getrefcount(x))    # 3

del y                         # 减一
print(sys.getrefcount(x))    # 2

循环引用:引用计数搞不定

class Node:
    def __init__(self):
        self.next = None

a = Node()
b = Node()
a.next = b
b.next = a       # 循环引用!

del a
del b
# 引用计数算上 a.next/b.next 永远不为 0 → 不会释放

gc 模块:处理循环引用

Python 的 垃圾回收器(gc) 定期扫描循环引用并打破:

import gc

gc.collect()              # 立刻跑一次
gc.get_count()            # 三代分别有多少待检查对象
gc.set_threshold(700, 10, 10)
gc.disable()              # 关闭(除非你知道在做什么)

99% 不需要管 gc——它自动跑。

weakref:弱引用避免循环

import weakref

class Cache:
    def __init__(self):
        self._data = weakref.WeakValueDictionary()

    def set(self, k, v):
        self._data[k] = v

    def get(self, k):
        return self._data.get(k)


cache = Cache()

class Big:
    pass

obj = Big()
cache.set("key", obj)
print(cache.get("key"))    # <Big object>

del obj                     # obj 被销毁
print(cache.get("key"))    # None  缓存自动清空

WeakValueDictionary 不会增加 value 的引用计数——key 还在但 value 消失。适合做缓存。

sys.getsizeof:单个对象多大

import sys

sys.getsizeof([])             # 56
sys.getsizeof([1, 2, 3])      # 88
sys.getsizeof("hello")        # 54
sys.getsizeof("中文")          # 64

注意:只算对象自身,不算它引用的对象

big_list = [list(range(1000)) for _ in range(1000)]
sys.getsizeof(big_list)        # 8056 (只算外层列表)
# 实际占用:每个内层 list ~8KB × 1000 = 8MB

算"递归大小":pympler

pip install pympler
from pympler import asizeof
asizeof.asizeof(big_list)      # 算所有引用对象的总大小

排查内存泄漏:tracemalloc

import tracemalloc

tracemalloc.start()

# ... 跑你的代码 ...

snapshot = tracemalloc.take_snapshot()
top = snapshot.statistics("lineno")
for stat in top[:10]:
    print(stat)

打印消耗内存最多的 10 行代码。

slots:减少类的内存

class WithoutSlots:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class WithSlots:
    __slots__ = ("x", "y")           # 固定属性集
    def __init__(self, x, y):
        self.x = x
        self.y = y


import sys
sys.getsizeof(WithoutSlots(1, 2))   # ~48 + __dict__
sys.getsizeof(WithSlots(1, 2))      # ~32,无 __dict__

百万级实例时省下大量内存。

实战:常见的内存泄漏

  1. 全局缓存无限长大
CACHE = {}                  # 长时间运行后会爆
def get(k):
    if k not in CACHE:
        CACHE[k] = expensive(k)
    return CACHE[k]

修复:用 functools.lru_cache(maxsize=...),或 WeakValueDictionary

  1. 闭包持有大对象
def make_handler(big_data):
    def handler(event):
        return event in big_data    # handler 持有 big_data
    return handler

handler 存在一天,big_data 就跟着存一天。

  1. 循环引用 + del

定义了 __del__ 的对象在循环引用里永远不会被 gc 回收——避免在有循环引用的类上写 __del__,用 __exit__ / 上下文管理器代替。

下一篇讲并发概览:线程 / 进程 / 异步。