0

0

Python中的垃圾回收机制是如何工作的?

紅蓮之龍

紅蓮之龍

发布时间:2025-09-04 08:39:02

|

238人浏览过

|

来源于php中文网

原创

Python的垃圾回收机制由引用计数和分代垃圾回收共同构成,前者实时释放无引用对象,后者周期性清理循环引用,两者协同确保内存高效管理。

python中的垃圾回收机制是如何工作的?

Python的垃圾回收机制,简而言之,就是一套自动管理内存的系统,它负责识别那些程序不再使用的对象,并将其占据的内存空间释放,以便后续可以重新分配。这套机制主要通过两种方式协同工作:核心的“引用计数”和用于处理特殊情况的“分代垃圾回收”(也称为循环垃圾回收)。

Python在内存管理上,其实是采取了一种混合策略,这背后有它深思熟虑的设计考量。它不像C++那样需要开发者手动管理内存,也不像Java那样完全依赖一个复杂的全局GC。理解它的工作原理,能帮助我们更好地编写高效且健壮的Python代码,尤其是在面对内存泄漏或性能瓶颈时。

引用计数是Python最基本、也是最直接的垃圾回收方式。每个Python对象内部都有一个引用计数器,记录着有多少个变量或数据结构指向它。当一个新引用指向对象时,计数器加一;当一个引用失效(比如变量超出作用域、被重新赋值或显式删除)时,计数器减一。一旦这个计数器归零,就意味着没有任何地方再“关心”这个对象了,Python解释器会立即回收它占用的内存。这种机制的优点是即时性强,内存可以很快被释放,减少了内存峰荷。

import sys

a = []
print(sys.getrefcount(a)) # 2 (a本身一个引用,getrefcount内部调用时也临时增加一个引用)

b = a
print(sys.getrefcount(a)) # 3 (多了一个b的引用)

del b
print(sys.getrefcount(a)) # 2 (b的引用被删除)

# 当a也被删除,引用计数归零,对象被回收
del a
# print(sys.getrefcount(a)) # 这里会报错,因为a已经不存在了

然而,引用计数并非完美无缺,它有一个致命的弱点:无法处理循环引用。当两个或多个对象互相引用,形成一个闭环,即使它们已经不再被外部任何代码所使用,它们的引用计数也永远不会降到零。比如,对象A引用了对象B,同时对象B又引用了对象A。在这种情况下,这两个对象会永远“活着”,即使它们对程序来说已经毫无意义,这就导致了内存泄漏。

立即学习Python免费学习笔记(深入)”;

为了解决这个问题,Python引入了“分代垃圾回收”机制,这是一种更高级的、可选的垃圾回收器,专门用来检测和清理循环引用。它不会像引用计数那样频繁运行,而是周期性地启动。这个垃圾回收器将所有对象分为三代(0代、1代、2代),新创建的对象都在0代。每次垃圾回收运行时,它会优先扫描0代对象,因为大部分对象的生命周期都比较短。如果一个对象在0代垃圾回收中幸存下来,它就会被“晋升”到1代;同样,1代幸存的对象会晋升到2代。扫描的频率会随着代数的增加而降低,因为高代数的对象往往生命周期更长,不值得频繁检查。

分代垃圾回收的核心是“标记-清除”(Mark-Sweep)算法。当它启动时,会遍历所有可能形成循环引用的容器对象(比如列表、字典、自定义类的实例等),暂时将它们的引用计数“冻结”或“归零”,然后从根对象(全局变量、栈帧中的局部变量等)出发,重新计算所有可达对象的引用计数。那些在重新计算后引用计数仍然为零的对象,就意味着它们是不可达的,可以被安全地回收。而那些在冻结期间,引用计数依然不为零,且重新计算后仍然不为零的对象,则被认为是可达的。通过这种方式,即使对象之间存在循环引用,只要它们整体上不可达,也能够被识别并清理。

为什么Python需要不止一种垃圾回收机制?

这是一个挺有意思的设计选择,也常常让人感到疑惑。简单来说,单一的引用计数机制虽然高效直接,但在处理特定场景时显得力不从心,尤其是在面对复杂的对象关系时。Python之所以不完全依赖分代回收,而是将引用计数作为主力,主要是基于性能和即时性的考量。

引用计数的好处是,当一个对象不再被引用时,内存几乎可以立即被释放。这对于大多数生命周期短、不涉及复杂引用的对象来说,效率非常高,避免了内存的长时间占用。想象一下,如果每次内存回收都要启动一个全局扫描,那程序的实时响应性会大打折扣。但正如前面提到的,引用计数有个致命的盲点:循环引用。两个对象互相指着对方,它们的引用计数永远不会降到零,即使外界已经没有办法访问到它们了。这就像两个困在孤岛上的人,彼此依赖,却无法回到大陆。

分代垃圾回收正是为了弥补引用计数的这个缺陷。它通过周期性的扫描,专门寻找那些引用计数不为零但实际上已经不可达的循环引用。这种机制的开销相对较大,因为它需要遍历和分析对象图,所以不能像引用计数那样频繁执行。Python的策略是,让引用计数处理绝大多数的内存回收工作,只有当引用计数无法解决的循环引用积累到一定程度时,才启动分代垃圾回收器进行一次“大扫除”。这是一种非常务实的折衷方案,既保证了日常操作的效率,又避免了内存泄漏的风险。可以说,这两种机制是相互补充,共同构成了Python健壮的内存管理体系。

引用计数是如何工作的,它有什么局限性?

引用计数的工作原理相对直观:每个Python对象都有一个内部计数器,记录着有多少个“引用”指向它。每当创建一个新的引用(比如将对象赋值给一个新变量),计数器就加1;每当一个引用失效(比如变量被删除、超出作用域或重新赋值),计数器就减1。当这个计数器减到零时,Python解释器就知道这个对象已经没有任何地方在使用了,于是会立即回收它所占用的内存。

我们可以用

sys.getrefcount()
函数来观察一个对象的引用计数,但需要注意的是,
getrefcount()
本身在调用时会临时增加一次引用,所以实际的引用计数会比
getrefcount()
返回的值少1。

import sys

my_list = [1, 2, 3]
print(f"初始引用计数: {sys.getrefcount(my_list) - 1}") # 1

another_ref = my_list
print(f"增加一个引用后: {sys.getrefcount(my_list) - 1}") # 2

def func():
    local_ref = my_list
    print(f"函数内部引用计数: {sys.getrefcount(my_list) - 1}") # 3
func()
print(f"函数结束后引用计数: {sys.getrefcount(my_list) - 1}") # 2 (local_ref超出作用域被回收)

del another_ref
print(f"删除一个引用后: {sys.getrefcount(my_list) - 1}") # 1

# 当my_list也被删除,引用计数归零,对象被回收
# del my_list

它的主要局限性,也是最让人头疼的一点,就是无法处理循环引用。设想一下这样的场景:

名品购物网店系统
名品购物网店系统

适合品牌专卖店专用,从前台的美工设计就开始强调视觉形象,有助于提升商品的档次,打造网店品牌!后台及程序核心比较简洁,着重在线购物,去掉了繁琐的代码及垃圾程式,在结构上更适合一些中高档的时尚品牌商品展示. 率先引入语言包机制,可在1小时内制作出任何语言版本,程序所有应用文字皆引自LANG目录下的语言包文件,独特的套图更换功能,三级物品分类,购物车帖心设计,在国内率先将购物车与商品显示页面完美结合,完

下载
class Node:
    def __init__(self, value):
        self.value = value
        self.next = None
        self.prev = None

a = Node('A')
b = Node('B')

a.next = b
b.prev = a

# 此时,a的引用计数因为b.prev指向它而增加,b的引用计数因为a.next指向它而增加。
# 即使我们删除了外部对a和b的引用:
del a
del b

# 理论上,我们已经无法通过任何外部变量访问到Node('A')和Node('B')了。
# 但实际上,Node('A')的next属性仍然指向Node('B'),Node('B')的prev属性仍然指向Node('A')。
# 它们的引用计数都不会降到零,因为彼此还在互相引用。
# 它们会一直占据内存,直到分代垃圾回收器介入。

这种互相依赖的关系,使得引用计数永远无法为零,从而导致内存泄漏。对于那些生命周期很长的应用,或者处理大量复杂数据结构的应用来说,这可能是一个潜在的性能隐患。

分代垃圾回收(Generational GC)是如何解决循环引用的?

分代垃圾回收器在Python中,就是专门为了解决引用计数无法处理的循环引用问题而设计的。它的核心思想是“标记-清除”(Mark-Sweep)算法,并结合了“分代”的概念来优化性能。

首先,它不是随时都在运行,而是根据一些阈值(比如分配了多少新对象、回收了多少旧对象)周期性地启动。当它启动时,并不会去检查所有对象,而是主要关注那些可能形成循环引用的“容器对象”(比如列表、字典、自定义类的实例等,因为只有它们才能包含对其他对象的引用),而像整数、字符串等不可变对象通常不会被检查,因为它们不可能参与循环引用。

解决循环引用的具体步骤大致是这样的:

  1. 识别潜在的循环对象:垃圾回收器会维护一个列表,记录那些可能参与循环引用的对象(主要是容器对象)。
  2. 模拟引用计数归零:在开始标记阶段前,垃圾回收器会暂时“忽略”这些对象之间的内部引用,并计算每个对象被外部(根对象,如全局变量、栈帧中的局部变量等)直接引用的次数。对于那些内部有引用,但外部引用计数为零的对象,它们就成了潜在的“垃圾”。
  3. 标记可达对象:从根对象出发,垃圾回收器会遍历所有可达的对象,并给它们打上“活”的标记。这个过程会重新计算它们的“有效”引用计数。
  4. 清除不可达对象:在标记阶段结束后,那些没有被打上“活”的标记,且其有效引用计数依然为零的对象,就被认为是不可达的垃圾,包括那些形成循环引用的对象。它们会被统一回收,释放内存。

分代机制则进一步优化了这个过程。它把对象分为0、1、2三代。新创建的对象都在0代。每次垃圾回收器运行时,优先检查0代对象,因为经验表明,大多数对象的生命周期都很短,很快就会变成垃圾。如果一个对象在0代的垃圾回收中幸存下来,它就会被晋升到1代;1代幸存的对象晋升到2代。高代数的对象被检查的频率更低,因为它们被认为是“老”对象,更可能长期存活。这种分代策略显著减少了每次垃圾回收需要扫描的对象数量,提高了效率。

通过这种“标记-清除”配合分代策略,Python的垃圾回收器能够有效地识别并清理那些被循环引用困住的内存,从而防止内存泄漏,保证程序的长期稳定运行。

我们能手动干预Python的垃圾回收吗?

虽然Python的垃圾回收机制是自动运行的,但在某些特定场景下,我们确实可以通过

gc
模块进行一些手动的干预或配置。这通常不是日常编程的常规操作,但对于调试内存问题、优化特定性能敏感的应用,或者处理一些特殊资源管理场景时,它会非常有用。

最直接的干预就是调用

gc.collect()
。这个函数会强制立即执行一次完整的垃圾回收。这在一些内存密集型操作之后可能会有用,例如:

import gc

def large_memory_operation():
    # 创建大量临时对象
    data = [list(range(1000)) for _ in range(1000)]
    # ... 进行一些处理 ...
    del data # 显式删除引用,但如果存在循环引用,内存可能不会立即释放

# 执行操作
large_memory_operation()

# 强制执行垃圾回收,以确保所有可回收的内存都被释放
collected = gc.collect()
print(f"手动回收了 {collected} 个对象。")

在一些长期运行的服务或批处理脚本中,如果发现内存占用持续增长,且怀疑存在循环引用导致的内存泄漏,手动调用

gc.collect()
可以作为一种临时的缓解措施。不过,过度频繁地调用它反而会引入额外的性能开销,因为垃圾回收本身就需要消耗CPU时间。

此外,

gc
模块还提供了一些配置选项:

  • gc.disable()
    gc.enable()
    :可以禁用或启用自动垃圾回收。在执行对性能要求极高的代码块时,如果确信不会产生循环引用,暂时禁用GC可以避免其带来的潜在中断。但要记住,务必在完成后重新启用,否则可能导致严重的内存泄漏。
  • gc.set_threshold(threshold0, threshold1, threshold2)
    :用于设置分代垃圾回收的触发阈值。当0代对象的分配数减去回收数达到
    threshold0
    时,0代GC就会被触发;如果0代GC触发次数达到
    threshold1
    ,则会触发1代GC,以此类推。调整这些阈值可以影响垃圾回收的频率和时机,但通常不建议随意修改,除非对GC的工作原理有深入理解并经过充分测试。
  • gc.set_debug(flags)
    :开启调试模式,可以打印垃圾回收过程中的详细信息,这对于排查内存问题非常有帮助。

总的来说,手动干预垃圾回收是一个强大的工具,但应该谨慎使用。在大多数情况下,Python的自动机制已经足够智能和高效。只有在明确知道自己在做什么,并且有充分理由时,才考虑介入GC的自动流程。过早的优化往往是万恶之源,对于垃圾回收也不例外。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
全局变量怎么定义
全局变量怎么定义

本专题整合了全局变量相关内容,阅读专题下面的文章了解更多详细内容。

78

2025.09.18

python 全局变量
python 全局变量

本专题整合了python中全局变量定义相关教程,阅读专题下面的文章了解更多详细内容。

96

2025.09.18

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

298

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

212

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1501

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

624

2023.11.24

java读取文件转成字符串的方法
java读取文件转成字符串的方法

Java8引入了新的文件I/O API,使用java.nio.file.Files类读取文件内容更加方便。对于较旧版本的Java,可以使用java.io.FileReader和java.io.BufferedReader来读取文件。在这些方法中,你需要将文件路径替换为你的实际文件路径,并且可能需要处理可能的IOException异常。想了解更多java的相关内容,可以阅读本专题下面的文章。

633

2024.03.22

php中定义字符串的方式
php中定义字符串的方式

php中定义字符串的方式:单引号;双引号;heredoc语法等等。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

588

2024.04.29

clawdbot ai使用教程 保姆级clawdbot部署安装手册
clawdbot ai使用教程 保姆级clawdbot部署安装手册

Clawdbot是一个“有灵魂”的AI助手,可以帮用户清空收件箱、发送电子邮件、管理日历、办理航班值机等等,并且可以接入用户常用的任何聊天APP,所有的操作均可通过WhatsApp、Telegram等平台完成,用户只需通过对话,就能操控设备自动执行各类任务。

14

2026.01.29

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
最新Python教程 从入门到精通
最新Python教程 从入门到精通

共4课时 | 22.4万人学习

Django 教程
Django 教程

共28课时 | 3.6万人学习

SciPy 教程
SciPy 教程

共10课时 | 1.3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号