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 程序长期运行。
相关推荐
重生之我是Java开发战士18 小时前
【Java EE】Spring IoC 与 DI
java·spring·java-ee
SimonKing18 小时前
阿里要求全员卸载 Claude Code:事件始末与深层逻辑
java·后端·程序员
云烟成雨TD18 小时前
Agent Scope Java 2.x 系列【42】2.0 GA 正式发布:从透明开发迈向智能体系统工程
java·人工智能·agent
猫猫不是喵喵.19 小时前
SpringBoot自动装配原理
java·spring boot·后端
刘小八19 小时前
Redis 缓存一致性:一文讲透延时双删原理、并发时序
java·数据库·redis·缓存
吃饱了得干活19 小时前
从0到1实现消息已读未读:从基础设计到高并发架构
java·后端·架构
r_oo_ki_e_19 小时前
java微服务
java·微服务
weixin_BYSJ198719 小时前
django在线图书销售平台---附源码16192
java·javascript·spring boot·python·django·flask·php
糖果店的幽灵19 小时前
【langgraph 从入门到精通graphApi 篇】Command 与动态流程控制
android·java·数据库·人工智能·langgraph
只会CRUD的码仔19 小时前
【踩坑记录】Thymeleaf 下拉框设置 disabled 变灰色,但依旧可以点击选择
java