Python 垃圾回收触发完整机制

目录

Python的垃圾回收机制是如何触发的?

[Python 垃圾回收触发完整机制](#Python 垃圾回收触发完整机制)

一、第一套:引用计数回收(实时、无额外触发条件)

触发规则

[什么情况会让 refcnt-1,可能触发回收](#什么情况会让 refcnt-1,可能触发回收)

局限性

[二、第二套:分代标记清除 GC(专门解决循环引用)](#二、第二套:分代标记清除 GC(专门解决循环引用))

[1. 自动触发(最常用,程序后台自动执行)](#1. 自动触发(最常用,程序后台自动执行))

[阈值读取 / 修改](#阈值读取 / 修改)

自动触发完整流程

[2. 手动强制触发(代码主动调用)](#2. 手动强制触发(代码主动调用))

[3. 程序退出时强制触发](#3. 程序退出时强制触发)

[三、特殊场景:GC 不会触发 / 暂时失效](#三、特殊场景:GC 不会触发 / 暂时失效)

四、触发逻辑极简总结表

如何使用Python的垃圾回收机制?

[Python 垃圾回收机制完整讲解:原理 + 手动使用 + 实操代码](#Python 垃圾回收机制完整讲解:原理 + 手动使用 + 实操代码)

一、底层核心:两套回收机制(自动运行,也可手动调用)

[1. 基础:引用计数(实时回收)](#1. 基础:引用计数(实时回收))

[2. 补充:分代标记清除(解决循环引用)](#2. 补充:分代标记清除(解决循环引用))

[二、如何手动使用垃圾回收(gc 标准库)](#二、如何手动使用垃圾回收(gc 标准库))

[1. 导入模块](#1. 导入模块)

[2. 常用 API(实操)](#2. 常用 API(实操))

[① 手动执行一次全量垃圾回收](#① 手动执行一次全量垃圾回收)

[② 查看 / 修改分代回收阈值(控制自动触发时机)](#② 查看 / 修改分代回收阈值(控制自动触发时机))

[③ 关闭 / 开启自动垃圾回收](#③ 关闭 / 开启自动垃圾回收)

[④ 查看当前待回收的循环引用对象](#④ 查看当前待回收的循环引用对象)

[⑤ del 手动减少引用(配合 GC 使用)](#⑤ del 手动减少引用(配合 GC 使用))

三、完整实操示例(循环引用手动回收)

[四、开发中正确使用 GC 的场景](#四、开发中正确使用 GC 的场景)

[场景 1:大量循环引用对象,内存暴涨,手动清理](#场景 1:大量循环引用对象,内存暴涨,手动清理)

[场景 2:调试内存泄漏](#场景 2:调试内存泄漏)

[场景 3:程序退出前释放全部内存](#场景 3:程序退出前释放全部内存)

五、关键注意点

[六、补充:C/C++ 嵌入 Python 时的回收(适配你的 Qt+C++ OpenCV 栈)](#六、补充:C/C++ 嵌入 Python 时的回收(适配你的 Qt+C++ OpenCV 栈))


Python的垃圾回收机制是如何触发的?

Python 垃圾回收触发完整机制

Python GC 分为两套体系:引用计数(即时触发)分代标记清除 GC(阈值自动触发 / 手动触发),两套触发逻辑完全分开。

一、第一套:引用计数回收(实时、无额外触发条件)

对应底层源码 PyObject.ob_refcnt

触发规则

只要对象的引用计数 ob_refcnt 递减到 0立刻同步触发回收,不需要等待、不需要 GC 扫描线程。 会自动释放该对象内存。

什么情况会让 refcnt-1,可能触发回收

  1. 变量被 del x 删除;
  2. 局部变量离开作用域(函数执行完毕、循环结束);
  3. 变量重新赋值:a = 1; a = 2,旧对象1引用 - 1;
  4. 容器(列表 / 字典)移除元素、容器销毁;
  5. 程序运行结束,所有变量全部销毁。

局限性

循环引用场景下,两个对象互相持有引用,refcnt 永远≥1,引用计数无法触发回收,必须靠第二套分代 GC 兜底。


二、第二套:分代标记清除 GC(专门解决循环引用)

Python 把对象分为 3 代:0代(新生代)1代2代(老年代),越老扫描频率越低。

1. 自动触发(最常用,程序后台自动执行)

核心:每代维护对象计数阈值,超出阈值自动触发对应代回收。

阈值读取 / 修改

python

复制代码
import gc
print(gc.get_threshold())  # 输出 (threshold0, threshold1, threshold2)
# 默认:(700, 10, 10)

参数含义:

  1. threshold0 = 700 0 代新建对象数量达到 700,触发 0 代 GC 扫描; 每次成功完成 0 代回收,会记录一次计数;累计满 10 次,0 代对象晋升到 1 代,触发 1 代扫描。
  2. threshold1 = 10 1 代累计满 10 次回收,对象晋升到 2 代,触发 1 代完整扫描;
  3. threshold2 = 10 2 代累计满 10 次晋升,触发全代完整 GC 扫描(开销最大)。
自动触发完整流程
  1. 创建新对象时,解释器会给 0 代计数器 + 1;
  2. 0 代计数 ≥700 → 触发 0 代 GC:
    • 扫描所有 0 代对象,找出循环引用、标记存活对象;
    • 销毁无存活引用的垃圾对象;
    • 存活对象晋升到 1 代,0 代回收计数 + 1;
  3. 0 代回收次数 ≥10 → 触发 1 代 GC,存活对象晋升 2 代;
  4. 1 代回收次数 ≥10 → 触发全代 GC(0+1+2 代全部扫描)。

2. 手动强制触发(代码主动调用)

不管当前计数是否达到阈值,立刻执行全代垃圾扫描:

python

复制代码
import gc
# 立刻执行完整GC,返回本次回收的对象数量
gc.collect()

# 只回收指定代(0/1/2)
gc.collect(0)  # 仅扫描0代
gc.collect(1)  # 扫描0+1代
gc.collect(2)  # 全代扫描,等同于无参collect()

适用场景:处理大量图像、YOLO 张量、内存泄漏调试时手动释放内存。

3. 程序退出时强制触发

Python 脚本 / 解释器关闭时,自动执行一次完整 GC,释放所有残留循环引用对象,避免内存残留。


三、特殊场景:GC 不会触发 / 暂时失效

  1. 手动关闭自动 GC

    python

    复制代码
    gc.disable()

    关闭后,分代 GC 不会自动触发,仅靠引用计数回收;循环引用对象会永久占用内存,直到程序结束。 恢复自动:gc.enable()

  2. 存在__del__析构函数的循环引用对象 GC 无法判断析构执行顺序,这类对象不会被自动回收,会存入gc.garbage列表永久滞留。

  3. 多线程 GIL 限制 GC 扫描全程需要持有 GIL 锁,线程密集场景会延迟 GC 触发时机。


四、触发逻辑极简总结表

表格

回收类型 触发方式 触发时机 处理对象
引用计数回收 自动即时触发 对象 refcnt == 0 无循环引用的普通对象(int、float、无嵌套容器)
分代 GC 自动触发 阈值触发 对应代新建对象 / 晋升次数达到阈值 存在循环引用的容器(list、dict、自定义类实例)
分代 GC 手动触发 代码调用gc.collect() 代码执行到该行时 全量循环引用垃圾,随时释放
全局收尾 GC 程序退出自动触发 解释器关闭最后阶段 全部未释放的残留对象

如何使用Python的垃圾回收机制?

Python 垃圾回收机制完整讲解:原理 + 手动使用 + 实操代码

一、底层核心:两套回收机制(自动运行,也可手动调用)

1. 基础:引用计数(实时回收)

对应你刚才看的源码 PyObject->ob_refcnt

  1. 规则
    • 对象被赋值、传入函数:ob_refcnt +=1
    • 变量销毁、函数退出、del 删除、程序结束:ob_refcnt -=1
    • ob_refcnt == 0:立刻释放对象内存
  2. 缺陷:循环引用无法回收

python

复制代码
# 循环引用案例,引用计数无法归零
a = []
b = []
a.append(b)
b.append(a)
del a, b
# 此时a、b互相引用,refcnt都是1,引用计数无法释放

2. 补充:分代标记清除(解决循环引用)

专门处理循环引用对象,Python 分 3 代(0/1/2),新对象放 0 代,扫描阈值触发自动回收。

二、如何手动使用垃圾回收(gc 标准库)

1. 导入模块

python

复制代码
import gc

2. 常用 API(实操)

① 手动执行一次全量垃圾回收

python

复制代码
# 返回本次回收释放的对象数量
released = gc.collect()
print("本次清理无效对象:", released)
② 查看 / 修改分代回收阈值(控制自动触发时机)

python

复制代码
# 获取三代阈值 (threshold0, threshold1, threshold2)
print(gc.get_threshold())

# 修改阈值:0代满500个对象就触发扫描
gc.set_threshold((500, 10, 5))
③ 关闭 / 开启自动垃圾回收

python

复制代码
gc.disable()   # 关闭自动GC,所有垃圾只靠引用计数
gc.enable()    # 恢复自动GC
gc.isenabled() # 返回True/False,查看自动GC是否开启
④ 查看当前待回收的循环引用对象

python

复制代码
gc.collect()
# 保存未释放、存在循环引用的对象列表
garbage_list = gc.garbage
print("残留垃圾对象:", garbage_list)
⑤ del 手动减少引用(配合 GC 使用)

del 变量名 作用:删除变量,对象引用计数 - 1,不会立刻释放内存,仅当 refcnt=0 才释放。

python

复制代码
s = [1,2,3]
del s  # 变量s销毁,列表refcnt-1,若无其他引用直接释放

三、完整实操示例(循环引用手动回收)

python

复制代码
import gc

# 制造循环引用
a = []
b = []
a.append(b)
b.append(a)

# 删除变量,此时两个列表互相引用,refcnt=1
del a, b

# 手动触发垃圾回收
clear_num = gc.collect()
print(f"清理了{clear_num}个循环引用对象")

# 查看残留垃圾
print("残留垃圾:", gc.garbage)

四、开发中正确使用 GC 的场景

场景 1:大量循环引用对象,内存暴涨,手动清理

处理图像、深度学习张量、大规模列表嵌套时,循环引用堆积,定期 gc.collect() 释放内存。

场景 2:调试内存泄漏

  1. gc.disable() 关闭自动回收
  2. 运行业务代码
  3. gc.collect() 手动回收
  4. 打印 gc.garbage 定位无法释放的对象,排查循环引用。

场景 3:程序退出前释放全部内存

脚本结束前执行一次 gc.collect(),主动清空内存,避免系统延迟释放。

五、关键注意点

  1. 绝大多数场景不用手动调用 gc Python 自动运行 GC,手动gc.collect()仅在内存泄漏、大数据场景优化使用;频繁调用会拖慢运行速度。
  2. 循环引用中,带__del__析构函数的对象回收会失效 自定义类写了__del__方法,出现循环引用时 GC 无法判断析构顺序,会永久存放在gc.garbage中无法释放。
  3. 引用计数是主线,GC 只是兜底优先避免循环引用,不要依赖手动 GC 解决代码设计问题。

六、补充:C/C++ 嵌入 Python 时的回收(适配你的 Qt+C++ OpenCV 栈)

如果你在 VS2015 C++ 代码嵌入 Python 解释器:

  1. C 层 API 手动回收:PyGC_Collect()
  2. 退出解释器前必须执行垃圾回收,否则 Python 堆内存泄漏,影响 Qt 程序长期运行。
相关推荐
vHelios19 小时前
【电商项目】商品服务模块的问题解决与代码逻辑思考
java·sql·mybatis
rannn_11119 小时前
【力扣hot100】238、41、73题解
java·算法·leetcode·开发
QN1幻化引擎20 小时前
认知场的涌现动力学:结构证明、Phi度量与意识签名电池
人工智能·深度学习·神经网络·算法·机器学习·agi
今天的砖头有点烫手啊20 小时前
Spring Boot
java·人工智能
坚持编程的菜鸟1 天前
模拟实现memmove
c语言·算法·模拟实现memmove
码农颜1 天前
5.4.1 锁分类
java·数据库·mysql
wabs6661 天前
关于图论【最短路径之Dijkstra算法(堆优化版)|卡码网47.参加科学大会的思考】
数据结构·算法·图论·优先级队列·邻接表·小顶堆·卡码网
坚持编程的菜鸟1 天前
编写判断大小端程序
c语言·算法·判断大小端
papaofdoudou1 天前
判断排列逆序奇偶性的乘积判别法(范德蒙德符号法)
人工智能·算法
linux-hzh1 天前
百日算法修炼 · Day 03
java·算法