Python 能够自动进行内存分配和释放,但了解 python 垃圾回收 (garbage collection, GC) 的工作原理可以帮助你写出更好更快的 Python 程序。Python 使用两种算法进行垃圾回收,分别是引用计数 (Reference Counting)分代回收 (Generational garbage collection)

引用计数

引用计数,简而言之就是如果没有变量引用某一对象,那么该对象将会被回收。Python 中的每个变量都是对对象的引用,而不是对象本身。例如,赋值语句只是给右侧对象或右侧变量所对应的对象建立一个引用;一个对象都可以有许多引用。

核心概念:变量是指向一个对象的指针;有n个变量指向某一个对象,那该对象的引用计数则为n,又称该对象有n个引用

import sys
a = [1, 2, 3]
b = a # 赋值操作本身不会对数据进行复制,仅仅是建立引用关系print(id(a), id(b))  # 4483101696 4483101696,变量a b的id相同,说明a b指向同一对象
print(sys.getrefcount(a))  # 3, 其中调用getrefcount函数会使引用+1

为了跟踪每个对象的引用次数,每个对象都有一个名为引用计数的额外属性,当创建或删除指向对象的指针时,该属性的值会相应的增加或减少。以下三种情况会使对象的引用次数增加:

  • 赋值运算
  • 参数传递
  • 将对象附加到容器对象中

  如果某对象引用计数属性的值为零,CPython 会自动调用该对象特定的内存释放函数。如果该对象还包含对其他对象的引用,那么所包含的其他对象的引用计数也会自动减少。因此,可以依次释放其他对象。

  值得注意的是,在函数、类和代码块(如if-else代码块)之外声明的变量称为全局变量(global variables)。通常,这些变量会一直存在直到 Python 进程结束。因此,全局变量引用的对象的引用计数永远不会下降到零。在python进程中,所有全局变量都存储在一个字典中,可以通过调用globals()函数来获取全局变量。那反过来呢?在代码块内(例如,在函数或类中)定义的变量则具有一个局部作用域,可以通过调用locals()函数来获取局部变量。当 Python 解释器执行完一个代码块时,它会破坏在块内创建的局部变量及其引用。

我们举个例子:

import sysfoo = []
print(sys.getrefcount(foo)) # 2 references, 1 from the foo var and 1 from getrefcountdef bar(a):print(sys.getrefcount(a))bar(foo) # 4 references, from the foo var, function argument, getrefcount and Python's function stack
print(sys.getrefcount(foo)) # 2 references, the function scope is destroyed

当你想删除全局或局部变量时,可以使用删除变量及其引用(而不是对象本身)的 del语句。这在 jupyter notebook中工作时通常很有用,因为在jupyter notebook中所有单元格变量都是全局变量。CPython 使用引用计数的主要原因是历史原因,现在有很多关于这种技术的弱点的争论。比如,有人认为现代的垃圾回收算法可以更高效,无需使用引用计数。引用计数算法存在很多问题,例如循环引用、线程锁定以及额外内存和性能开销。必须指出的是,引用计数是 Python 无法摆脱全局解释锁 (GIL) 的原因之一。

分代回收

上面讲到了引用计数的缺点包括循环引用、线程锁定以及额外内存和性能开销。线程锁定,所以在python中使用多线程;额外内存和性能开销,我们也认了;但是循环引用的问题不解决的话,就会造成内存泄露问题。因此,python引入了分代回收的算法专门来解决循环引用的问题。

引用计数算法非常有效和直接,但它无法检测循环引用,所以python在引用计数的基础上,还需要分代回收。引用计数是 Python必需的功能,不能禁用;而分代回收是可选的,可以手动设置。

如上图示例所示,lst对象指向自身,而且Object 1Object 2 相互引用。在这两种情况下,这些对象的引用数永远至少为1。我们可以用代码来演示一下:

import gc
import sys
import ctypes# 通过内存地址去访问没有引用的对象(unreachable objects)
class PyObject(ctypes.Structure):_fields_ = [("refcnt", ctypes.c_long)]gc.disable()  # 禁用分代回收算法
lst = []
lst.append(lst)
lst_address = id(lst)
del lstobject_1, object_2 = {}, {}object_1['obj2'] = object_2
object_2['obj1'] = object_1
obj_address = id(object_1)
del object_1, object_2# 手动对象回收
# gc.collect()# 获取对象引用数量
print(PyObject.from_address(obj_address).refcnt)
print(PyObject.from_address(lst_address).refcnt)# 或者通过以下方式获取引用数量
# tmp = PyObject.from_address(obj_address)
# print(sys.getrefcount(tmp))# # output
1
1
2

在上面的示例中,del语句删除了对我们对象的引用(引用计数减 1)。 Python 执行 del语句后,我们的对象不再可以从 Python 代码访问。但是,这些对象仍然存在于内存中。发生这种情况是因为它们仍在相互引用,并且每个对象的引用计数为 1。因为我们前面通过gc.disable()禁用了分代回收,因为循环引用对象无法释放;这时,我们可以通过调用gc.collect()手动触发对象回收。

  我们知道,在python中对象分为可变对象和不可变对象。不可变对象包括int, float, complex, strings, bytes, tuple, rangefrozenset;可变对象包括list, dict, bytearrayset。循环引用仅存在于container对象(比如,list, dict, classes, tuples),python垃圾回收算法主要追踪可变对象及不可变对象tuple。如果tuple, dict包含的元素都是不可变对象,那么回收算法可以不对该对象进行追踪。

垃圾回收的触发

不同于引用计数,循环引用的垃圾回收不是实时作用的,而是定期运行。垃圾回收器将container对象分成三代(0, 1, 2),每个新对象都从第一代开始。如果一个对象在一个垃圾回收轮次中幸存下来,它将移至较旧(更高)的一代。较低代的回收频率高于较高代,因为大多数新对象往往会被先销毁。这样分代回收的策略能提高性能并减少垃圾回收带来的暂停时间。

  为了决定何时进行一轮垃圾回收,每一代都有一个单独的计数器和阈值。计数器存储自上次收集以来的对象分配数减去释放数的差值。每次分配新的容器对象时,CPython 都会检查第0代的计数器是否超过阈值(通过gc.get_count()获得三代对象计数器存储的数值)。如果超过阈值,Python 将触发垃圾回收。我们可以通过gc.get_threshold()gc.set_threshold()查看、设置阈值:

import gc
gc.get_threshold()  # (700, 10, 10) 分别对应三代计数器的阈值
gc.set_threshold(threshold0=800, threshold0=10, threshold0=10)  # 当threshold0设置为0时,禁用循环GC

  在写程序时,可以通过将调试标志设置为gc.DEBUG_SAVEALL,从而将所有unreachable对象添加到gc.garbage 中,帮助提升程序质量:

import gcgc.set_debug(gc.DEBUG_SAVEALL)print(gc.get_count())
lst = []
lst.append(lst)
list_id = id(lst)
del lst
gc.collect()
for item in gc.garbage:print(item)

参考

python documentation: GC

Garbage collection in Python: things you need to know

stacloverflow reference count

python垃圾回收 (GC) 机制相关推荐

  1. python gc教程_python中的垃圾回收(GC)机制

    一.引用计数 Python 垃圾回收以引用计数为主,分代回收为辅.引用计数法的原理是每个对象维护一个ob_refcnt,用来记录对象被引用的次数,也就是用来追踪有多少个引用指向了对象,当发生以下四种情 ...

  2. Python垃圾回收(gc)拖累了程序执行性能?

    起因 前段时间,在做文本处理的实验时,需要预加载大量的原始数据(100W),在Python中使用的字典(dict)类型负责保存这些数据,很快就开发完成 了一个Demo版,然而程序执行的效率不是那么令人 ...

  3. [JVM-3]Java垃圾回收(GC)机制和垃圾收集器选择

    哪些内存需要回收? 1.引用计数法 这个算法的实现是,给对象中添加一个引用计数器,每当一个地方引用这个对象时,计数器值+1:当引用失效时,计数器值-1.任何时刻计数值为0的对象就是不可能再被使用的.这 ...

  4. python垃圾回收机制(GC)相关问题

    在使用python中很少遇到内存溢出的问题,也不关心内存的管理问题,这是高级语言自带的处理机制,将内部的垃圾空间清除. 要清楚是怎么回收垃圾的,那我们应该先明白什么情况下产生垃圾,就涉及到python ...

  5. python 垃圾回收机制

    DAY 18. python垃圾回收机制 python GC主要有三种方式 引用计数 标记清除 分代回收 其中,以引用计数为主. 18.1 引用计数(Reference Counting) <寻 ...

  6. python进阶19垃圾回收GC

    原创博客链接:python进阶19垃圾回收GC 垃圾收集三大手段 一.引用计数(计数器) Python垃圾回收主要以引用计数为主,分代回收为辅.引用计数法的原理是每个对象维护一个ob_ref,用来记录 ...

  7. python垃圾回收离职_垃圾回收gc.md

    垃圾回收gc python的垃圾收回机制不想c和c++是开发者自己管理维护内存的,python的垃圾回收是系统自己处理的,所以作为普通的开发者,我们不需要关注垃圾回收部分的内容,如果想要深层次理解py ...

  8. 简述Python垃圾回收机制

    引言 许多高级语言都具有自己的垃圾回收机制,以管理计算机内存,Python也不例外.对于垃圾回收机制的了解程度,成了开发人员是否真正了解Python的检验手段,在面试的时候许多面试官也喜欢以此作为题目 ...

  9. python多线程详解 Python 垃圾回收机制

    文章目录 python多线程详解 一.线程介绍 什么是线程 为什么要使用多线程 总结起来,使用多线程编程具有如下几个优点: 二.线程实现 自定义线程 守护线程 主线程等待子线程结束 多线程共享全局变量 ...

最新文章

  1. 【Ubuntu】解决ubuntu系统root用户下Chrome无法启动问题
  2. linux 本地做yum源,linux——制作本地yum源
  3. hadoop元数据mysql中表字段_hive mysql元数据表说明
  4. Java基础学习总结(88)——线程创建与终止、互斥、通信、本地变量
  5. Android LoaderManager原理剖析
  6. net core引用外部dll发布后报错找不到文件
  7. 队列同步器(AbstractQueuedSynchronizer)源码简析
  8. 需求分析之矩阵分析法
  9. Google广告分类体系
  10. 2014中国互联网安全大会
  11. 云主机是什么,如何才能选择性价比高的云主机?
  12. 为何能力越强越不被重用?不懂这3点,你到哪里都混不好,不服不行
  13. 雷迪9000使用说明_雷迪司UPS监控软件使用说明中文
  14. 轮播移动端 html,移动端h5如何使用轮播插件swipe
  15. 视频播放器源码(WinForm)
  16. 【openjudge】金银岛
  17. 修复Lsp解决不能上网的问题
  18. 计算机多媒体对语文教学的提高,语文教学中多媒体教学运用心得
  19. xss攻击之盗取账号
  20. 豆瓣FM-Hacker——豆瓣FM播放列表补全计划

热门文章

  1. scala学习 之 环境搭建(一)
  2. Python3之configparser模块
  3. python多分类混淆矩阵代码_深度学习自学记录(3)——两种多分类混淆矩阵的Python实现(含代码)...
  4. 计算机视觉结课论文,计算机视觉与图像识别结课论文
  5. php中的rtrim_php中ltrim()、rtrim()与trim()删除字符空格实例
  6. android中判断sim卡状态和读取联系人资料的方法
  7. java与c/c++进行socket通信的一些问题
  8. java原型链_深入总结Javascript原型及原型链
  9. css3蒙版运动,纯CSS3制作逼真的汽车运动动画
  10. apache证书不受信任_苹果iOS手动安装和信任企业级应用