跳到主要内容

内存管理

程序在运行过程中,会不断创建对象(如列表、字符串、实例)。这些对象需要占用内存,如果只创建不释放,内存就会被耗尽。因此必须有一套机制来解决两个问题:

  • 什么时候应该释放一个对象?
  • 由谁、以什么方式释放?

在 Python 中,这个问题以引用计数为主、分代回收为辅来解决。

引用计数 (Reference Counting)

每个对象内部都会维护一个整数:引用计数(refcount),表示有多少个“引用”指向该对象。创建对象时会产生第一个引用,引用计数在符合条件情况下会增加或减少,当引用计数为 0 时,Python 会立即释放该对象占用的内存。

引用计数 增加 (+1) 情况:

  • 变量赋值:b = a
  • 绑定对象:a = [1, 2, 3]
  • 作为容器元素:lst.append(a)
  • 作为对象属性:obj.attr = a

引用计数 减少 (-1) 情况:

  • 变量删除:del a
  • 变量重新赋值:a = 123
  • 函数调用结束:func(a) 执行完毕后,参数 a 的引用计数减少
  • 从容器中移除引用/容器被销毁:lst.remove(a)/del lst
  • 对象属性被删除:del obj.attr

当引用计数 = 0 时,Python 立即释放内存

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

del a # refcount = 2
del b # refcount = 1
del c # refcount = 0 → 对象被销毁

因此,del 语句不一定会立即释放内存,只有当对象的引用计数变为 0 时才会被销毁。

优点

  • 实时回收:对象一旦不再被引用,内存就会立即释放,不需要等待 GC 触发。
  • 实现简单:每个对象维护一个整数,增加/减少时更新即可。

缺点

循环引用,如果两个或多个对象互相引用,导致它们的引用计数永远不为 0,就会造成内存泄漏。以下列代码为例:

a = []
b = []

a.append(b)
b.append(a)

del a
del b

此时,a 引用了 bb 引用了 a,引用计数永远 refcount ≥ 1,导致内存泄漏。因此,Python 引入了分代垃圾回收机制来解决循环引用问题。

分代垃圾回收 (Generational Garbage Collection)

分代垃圾回收专门处理引用计数无法解决的循环引用。分代 GC 会扫描容器对象(例如 list、dict、set 或自定义类实例),识别并回收那些形成了循环引用、已无法被外部访问的“孤岛”内存。

非容器对象(如整数、字符串等不可变类型)不会造成循环引用,分代 GC 通常不主动处理它们——它们要么靠引用计数回收,要么是静态/不可变的不需要回收。

分代机制

分代原则:对象活得越久,越不容易死

  • 第 0 代:新创建的对象,垃圾回收频率最高。
  • 第 1 代:从第 0 代晋升的对象,垃圾回收频率较低。
  • 第 2 代:从第 1 代晋升的对象,垃圾回收频率最低。

工作流程:

  • 触发条件:当第 0 代的对象数量超过预设的阈值时,GC 被触发。
  • 标记:GC 会遍历第 0 代的对象,追踪对象间的引用关系,找出那些“无法从根对象(如全局变量、调用栈)访问到”的循环引用组。
  • 清理:这些孤立的循环引用组会被整体销毁,内存被回收。
  • 晋升:回收后依然存活的对象,会晋升到下一代。

GC 处理实例

import gc

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

def __repr__(self):
return f"<Node {self.name}>"

# 1. 创建两个对象
a = Node("A")
b = Node("B")

# 2. 形成循环引用
a.ref = b # a 引用 b
b.ref = a # b 引用 a

# 3. 删除外部引用
del a
del b

# 4. 手动触发垃圾回收
print("触发垃圾回收前")
print(gc.collect()) # 返回被回收对象的数量

print("触发垃圾回收后")

输出:

触发垃圾回收前
4
触发垃圾回收后

注意:gc.collect() 返回的数量表示“被回收的对象总数”,而不是我们代码中“显式创建的对象数量”。

在上述代码例子中,实际参与 GC 的对象结构如下:

a (Node)
└── __dict__ (dict)
└── ref → b

b (Node)
└── __dict__ (dict)
└── ref → a

因此,被 GC 跟踪并最终回收的对象包括:

  • a(Node)
  • b(Node)
  • a.dict(dict)
  • b.dict(dict)

总共 4 个对象被回收。

拓展:__slots__

__slots__ 本质上是用“静态结构”替代“动态字典”,从而减少对象的内存布局开销,同时减少 GC 跟踪对象数量。以下列代码为例,如果使用 __slots__

class Node:
__slots__ = ('name', 'ref')

对象结构变成:

a (Node)
└── ref → b

b (Node)
└── ref → a

此时,实例不再包含 __dict__,少了两个 dict 对象参与 GC,gc.collect() 返回 2

在高密度对象场景(如数据建模、图结构)中,可以显著优化内存和 GC 性能,但牺牲了 Python 的动态性。

循环引用 vs 循环导入

  • 循环导入是代码组织/模块依赖问题,发生在程序启动阶段
  • 循环引用是内存管理问题,发生在程序运行阶段