在编程世界中,高效的内存管理是保证程序稳定运行的关键之一。然而Python作为一门高级编程语言,其自动垃圾回收(Garbage Collection, GC)常常被Pythoner们当做理所当然的黑盒,可一旦线上出现内存飙升,GC就成了最熟悉的陌生人。今天就来拆开这个黑盒,一窥究竟,看看Python是如何优化的帮我们进行垃圾回收的。
Python的垃圾回收主要依赖于引用计数,并在此基础上引入标记-清除和分代回收两种机制,以解决循环引用和提升回收效率的问题。
引用计数:最朴素的记账法
每个Python对象中都包含一个名为PyObject的基础结构体,其中,ob_refcnt字段即为该对象的引用计数,其基本规则如下:
- 当对象被新的引用指向时 -> ob_refcnt++;
- 当某个引用被删除或离开作用域时,del -> ob_refcnt–;
- ob_refcnt ==0 -> 一旦引用计数归零,对象会被立即回收。
这样做的可以让实现变得简单,逻辑很直观,于此同时,实时性很强,内存可立即被释放;当然,这样做也会有一些局限性,比如维护计数需要额外开销、无法处理循环引用(即:两个或多个对象相互引用,却无外部引用指向它们 )的情况等问题。
1 2 3 4 5 6 7 8
| import sys
a = [1, 2, 3] b = a print(sys.getrefcount(a))
del b
|
标记-清除:解决循环引用的利器
针对引用计数无法处理的循环引用问题,Python引入了标记-清除(Mark & Sweep)机制。其基本思路是这样的:
- 先按需分配内存,直到空闲内存不足时触发垃圾回收;
- 从根对象(如寄存器和程序栈中的引用)出发,遍历所有可达对象,并对其进行标记;
- 遍历结束后,清除所有未被标记的对象,释放其占用的内存空间。
通过这样的处理方式,即使对象之间存在循环引用,只要它们已经不再被实际使用,就会被正确回收。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| import gc import psutil import os
def rss_mb(): """获取当前进程占用的物理内存 (MB)""" return psutil.Process(os.getpid()).memory_info().rss // 1024 // 1024
class Node: """一个会互相引用的小节点""" def __init__(self, value): self.value = value self.partner = None
def __repr__(self): return f"Node({self.value})"
print("Step 1: 制造循环引用") print("RSS before:", rss_mb(), "MB") nodes = [] for i in range(1_000_000): a, b = Node(i), Node(-i) a.partner = b b.partner = a nodes.extend([a, b])
print("RSS after create:", rss_mb(), "MB")
del nodes print("RSS after del nodes:", rss_mb(), "MB")
gc.collect() print("RSS after gc.collect():", rss_mb(), "MB")
|
分代回收: 以空间换时间的优化策略
为了进一步提升回收的效率,Python使用了分代回收的策略。该策略将内存中的对象按其存活时间划分为不同的集合(称为“代”,Generational GC),Python默认定义了三代:
- 第0代:新创建的对象;
- 第1代:经历过一次垃圾回收后仍然存活的对象;
- 第2代:存活时间更长的对象。
回收频率随代号的增加而降低,即代数越高,GC越“懒得管”。比如说:
- 新分配的对象会被放在第0代(GC的常客,回收最勤快);
- 如果它们经历过一次GC后仍然存活,则被移至第1代(偶尔光顾);
- 若再经历一次GC仍未被回收,则进入第二代(几乎佛系,除非内存告急)。
这样做的好处是:大部分新对象往往很快就不再使用,而对“年轻代”进行频繁回收,可以在更短时间内清理掉大部分垃圾,而检查老一代对象的频率较低,从而减少了整体的开销。通俗的说,这种“年纪越大越免检”的策略,让GC把80%的精力花在最容易产生垃圾的第0代,整体吞吐量大幅提升。
1 2 3 4 5 6 7 8 9
| import gc
print(gc.get_threshold())
gc.collect() gc.collect(0) gc.collect(1)
|
总结:
Python的垃圾回收机制是一个结合了引用计数、标记-清除和分代回收的复合系统。它既保证了内存回收的实时性,也有效解决了循环引用问题,并通过分代策略实现了高性能的自动内存管理。
了解这一机制后,我们在后续的编码中可以考虑:
- 尽量使用局部变量,缩短对象生命周期
- 手动断开长引用链:a.b = None
- 调优GC:
get_threshold()
, 或在低延迟场景下,gc.disable()
后手动触发。
另外,在日常开发中如果遇到了OOM相关问题,可以尝试分析的思路有:
- 实时监控内存使用
- 定位内存增长点
- 先确定内存中一直保持增长的是什么类型的对象,看看内存分配最频繁的地方是哪里;
- 分析大对象
- 将重点怀疑的对象通过工具输出引用关系(如:objgraph),从而进一步分析出内存的问题源自哪里。
有了上述的思路后,可以尝试借助下列工具进行更进一步的问题分析和定位:
下列工具的具体使用方法,可点击链接前往官方文档进行学习使用。