Python的垃圾回收机制详解
Python的垃圾回收(Garbage Collection,GC)机制主要基于引用计数,并辅以分代回收和标记-清除来解决循环引用问题。下面分层详细说明:
1. 引用计数(Reference Counting)
核心原理:每个对象维护一个计数器,记录指向它的引用数量。当引用计数归零时,对象立即被回收。
引用变化场景:
-
增加计数:对象被创建、赋值、加入容器、作为参数传递
-
减少计数:引用被删除、离开作用域、容器被销毁
示例:
import sys# 创建对象,引用计数=1
a = [1, 2, 3]
print("一开始引用计数:",sys.getrefcount(a)) # 输出2(getrefcount调用产生临时引用)# 增加引用
b = a # 引用计数+1
print("赋值给b的引用计数:",sys.getrefcount(a)) # 输出3# 减少引用
del b # 引用计数-1
print("销毁b的引用计数:",sys.getrefcount(a)) # 输出2(回到初始状态)# 离开作用域
def func():c = a # 函数内引用print("函数引用计数:",sys.getrefcount(a))func() # 函数结束,c销毁,引用计数-1
print("函数销毁后引用计数:",sys.getrefcount(a)) # 输出2
1.创建对象,引用计数+1
2.getrefcount方法作用获取对象的引用计数
3.调用getrefcount()引用计数加1
4.b=a,a对象引用计数+1
5.del b,a对象引用计数-1
6.func()里c=a,a对象引用计数+1
7.函数结束,c对象自动销毁,a对象计数-1
8.最后程序结束。垃圾(内存)全部回收。
这是基本的一段基本垃圾回收的演示
2.循环引用导致内存泄漏
class Node:def __init__(self):self.parent = Noneself.child = None# 创建循环引用
x = Node()
y = Node()
x.child = y # x引用y
y.parent = x # y引用x# 删除外部引用
del x
del y# 此时两个对象引用计数均为1(相互引用),无法被引用计数回收
当两个对象相互引用时,引用计数永不归零,导致内存泄漏。
这样会是什么结果?
比如说长时间运行的应用程序 ,在长时间运行的 Python 应用程序中,比如一个长时间运行的 Web 服务器或者后台数据处理服务。如果频繁创建这种具有循环引用的对象结构,并且没有被正确回收,内存泄漏会随着时间推移而积累。例如,在一个处理大量文档的系统中,每个文档对象可能包含对其他相关对象(如章节对象、段落对象等)的引用,如果这些对象之间形成循环引用并且没有被正确管理,会占用越来越多的内存。内存泄漏的内存去向,在未执行完进程的内存空间,未被回收的内存仍然归属于运行程序的进程。操作系统不会收回这部分内存,因为它认为这些内存仍然被进程占用。当然如果程序很快就执行完,那这个内存泄漏基本可以忽略了,因为程序一旦运行完,回收全部垃圾。
这样不明显,我们可以引入psutil库来获取程序的实时内存情况
import psutil
import osdef get_memory_usage():process = psutil.Process(os.getpid())return process.memory_info().rss / (1024 * 1024) # 单位:MB
举例一个不内存泄漏程序:
import gc
import psutil
import os
import timeclass Node:def __init__(self):self.parent = Noneself.child = Nonedef get_memory_usage():process = psutil.Process(os.getpid())return process.memory_info().rss # 单位:Bytedef no_memory_leak_example():for _ in range(100000):x = Node()y = Node()x.child = y # x引用yy.parent = x # y引用x# 手动打破循环引用x.child = Noney.parent = Nonedel xdel ydef main():initial_memory = get_memory_usage()print(f"初始内存使用:{initial_memory:.2f} Byte")no_memory_leak_example()final_memory = get_memory_usage()print(f"最终内存使用:{final_memory:.2f} Byte")if __name__ == "__main__":main()
通过get_memory_usage()函数监控运行前和运行完前一步的内存变化。
可以看出,内存波动很小 。
那内存泄漏程序呢?
import gc
import psutil
import os
import timeclass Node:def __init__(self):self.parent = Noneself.child = Nonedef get_memory_usage():process = psutil.Process(os.getpid())return process.memory_info().rss # 单位:Bytedef no_memory_leak_example():for _ in range(100000):x = Node()y = Node()x.child = y # x引用yy.parent = x # y引用x# 手动打破循环引用del xdel ydef main():initial_memory = get_memory_usage()print(f"初始内存使用:{initial_memory:.2f} Byte")no_memory_leak_example()final_memory = get_memory_usage()print(f"最终内存使用:{final_memory:.2f} Byte")if __name__ == "__main__":main()
咋一看,好像波动也小。主要原因还是程序太快了,我们加大循环。
差别更大了,但实际运行时间还是很短。如果对于已经部署云服务器的大项目,因为是长时间运行,一定要规避内存泄漏,不然,无用内存占用越来越多。
那如何避免内存泄漏呢?
3.分代回收回收垃圾
为了监控垃圾回收情况,可以使用 gc
模块来跟踪垃圾回收器的活动,包括未被回收的对象数量和垃圾回收的次数等。
分代回收
分代回收(Generational Garbage Collection)是一种基于对象生命周期的垃圾回收策略。其核心思想是:大多数对象的生命周期都很短,存活时间长的对象往往也会继续存活更久。因此,将对象按年龄(经历过的垃圾回收次数)分到不同的代(generation),并针对不同代采用不同的回收频率,可以显著提高垃圾回收的效率。
在Python中,分代回收作为引用计数的补充,主要解决循环引用问题。Python将对象分为三代:
- 第0代(generation 0):新创建的对象
- 第1代(generation 1):经历过一次0代垃圾回收后仍然存活的对象
- 第2代(generation 2):经历过一次1代垃圾回收(或多次0代回收)后仍然存活的对象
默认值通常是`(700, 10, 10)`,表示:
700:0 代对象分配数达到 700 时触发 0 代回收
10:0 代回收发生 10 次后触发 1 代回收
10:1 代回收发生 10 次后触发 2 代回收
**垃圾回收触发条件**:
- 当分配新对象的数量减去释放的对象的数量超过某个阈值(threshold)时,就会触发垃圾回收。
- Python使用三个阈值(threshold0, threshold1, threshold2)分别控制三代垃圾回收的触发。
** 垃圾回收总是从第0代开始**:
- 如果第0代垃圾回收发生了多次(具体次数由阈值threshold1控制),那么就会触发一次第1代垃圾回收。
- 如果第1代垃圾回收发生了多次(具体次数由阈值threshold2控制),那么就会触发一次第2代垃圾回收。
- 在进行第N代回收时,比它年轻的所有代(第0代到第N-1代)也会被一起回收。
基本流程
垃圾回收案例如下
import gc
import psutil
import os
import timeclass Node:def __init__(self):self.parent = Noneself.child = Nonedef get_memory_usage():process = psutil.Process(os.getpid())return process.memory_info().rss # 单位:Bytedef no_memory_leak_example():# 创建大量循环引用对象for i in range(10000000):x = Node()y = Node()x.child = y # x引用yy.parent = x # y引用x# 删除引用 - 对象现在只有相互引用del xdel y# 定期触发垃圾回收if i % 5000 == 0:# 只回收最新一代对象(0代),开销较小gc.collect(0)def main():# 获取初始内存使用initial_memory = get_memory_usage()print(f"初始内存使用:{initial_memory:,} Byte")# 获取当前垃圾回收阈值thresholds = gc.get_threshold()print(f"默认GC阈值:gen0={thresholds[0]}, gen1={thresholds[1]}, gen2={thresholds[2]}")# 记录开始时间start_time = time.time()# 执行创建循环引用对象的函数no_memory_leak_example()# 记录结束时间end_time = time.time()# 强制触发完整垃圾回收print("触发完整垃圾回收...")gc.collect()# 获取最终内存使用final_memory = get_memory_usage()print(f"最终内存使用:{final_memory:,} Byte")print(f"内存变化:{final_memory - initial_memory:,} Byte")print(f"执行时间:{end_time - start_time:.2f} 秒")# 打印GC统计信息print("\nGC统计信息:")print(f"0代回收次数: {gc.get_stats()[0]['collections']}")print(f"1代回收次数: {gc.get_stats()[1]['collections']}")print(f"2代回收次数: {gc.get_stats()[2]['collections']}")print(f"回收对象总数: {sum(gen['collected'] for gen in gc.get_stats())}")if __name__ == "__main__":# 启用GC(确保它是启用的)gc.enable()# 设置更积极的GC阈值以减少内存峰值gc.set_threshold(500, 10, 10) # 比默认(700, 10, 10)更频繁main()
最终结果:
事实上按循环引用,产生垃圾对象个数是10000000*2,而回收垃圾对象是20001247,最终内存会比初始内存小。
def no_memory_leak_example():# 创建大量循环引用对象for i in range(10000000):x = Node()y = Node()x.child = y # x引用yy.parent = x # y引用x# 删除引用 - 对象现在只有相互引用del xdel y
最终内存会比初始内存小原因 Python 解释器启动时加载的模块和资源所产生内存也可能被操作系统回收。
总结:
Python的垃圾回收机制以引用计数为主,标记清除和分代回收为辅。引用计数可以实时回收不再使用的对象,而分代回收则负责处理循环引用,并按代管理对象以提高效率。在大多数情况下,开发者无需关心垃圾回收,但在处理循环引用或性能敏感场景时,了解这些机制有助于优化程序。