Python程序在运行的时候,需要在内存中开辟出一块空间用于存放运行时产生的临时变量,计算完成后再将结果输出到永久性存储器中
如果数据量过大,内存空间管理不善就很容易出现OOM(out of memory),程序可能被操作系统中止

对于服务器,这种设计为永不中断的系统来说内存管理则显得更为重要,不然很容易引发内存泄漏。什么是内存泄漏呢?

  • 程序本身没有设计好,导致程序未能释放已不再使用的内存
  • 并不是内存在物理上消失,而是意味着代码在分配了某段内存后,因为设计错误失去了对这段内存的控制,从而造成了内存的浪费

那么,Python又是怎么解决这些问题的?换句话说,对于不会再用到的内存空间,Python是通过什么机制来回收这些空间的呢?

一、计数引用

Python中一切皆对象。因此,所看到的一切变量本质上都是对象的一个指针

那么,怎么知道一个对象是否永远都不能被调用了呢?

非常直观的一个想法,就是当这个对象的引用计数(指针数)为0的时候,说明这个对象永不可达,自然它也就成为了垃圾需要被回收

1.1 局部变量内存使用和释放

看一个例子:

import os
import psutil# 显示当前 python 程序占用的内存大小
def show_memory_info(hint):pid = os.getpid()p = psutil.Process(pid)info = p.memory_full_info()memory = info.uss / 1024. / 1024print('{} memory used: {} MB'.format(hint, memory))
def func():show_memory_info('initial')a = [i for i in range(10000000)]show_memory_info('after a created')func()
show_memory_info('finished')########## 输出 ##########initial memory used: 47.19140625 MB
after a created memory used: 433.91015625 MB
finished memory used: 48.109375 MB

通过这个示例,可以看到调用函数func(),在列表a被创建之后内存占用迅速增加到了433MB,而在函数调用结束后内存则返回正常

这是因为,函数内部声明的列表a是局部变量,在函数返回后局部变量的引用会注销掉,此时,列表a所指代对象的引用数为0,Python便会执行垃圾回收,因此占用的大量内存被释放

1.2 全局变量内存使用和释放

明白这个原理后,稍微修改一下代码:

def func():show_memory_info('initial')global aa = [i for i in range(10000000)]show_memory_info('after a created')func()
show_memory_info('finished')########## 输出 ##########initial memory used: 48.88671875 MB
after a created memory used: 433.94921875 MB
finished memory used: 433.94921875 MB

新的这段代码中,global a表示将a声明为全局变量
那么,即使函数返回后列表的引用依然存在,于是对象就不会被垃圾回收掉,依然占用大量内存

1.3 主程序引用的变量内存使用和释放

同样,如果把生成的列表返回然后在主程序中接收,那么引用依然存在,垃圾回收就不会被触发,大量内存仍然被占用

def func():show_memory_info('initial')a = [i for i in derange(10000000)]show_memory_info('after a created')return aa = func()
show_memory_info('finished')########## 输出 ##########initial memory used: 47.96484375 MB
after a created memory used: 434.515625 MB
finished memory used: 434.515625 MB

1.4 内部引用计数机制

这是最常见的几种情况
由表及里,下面深入看一下Python内部的引用计数机制。先来看代码:

import sysa = []# 两次引用,一次来自 a,一次来自 getrefcount
print(sys.getrefcount(a))def func(a):# 四次引用,a,python 的函数调用栈,函数参数,和 getrefcountprint(sys.getrefcount(a))func(a)# 两次引用,一次来自 a,一次来自 getrefcount,函数 func 调用已经不存在
print(sys.getrefcount(a))########## 输出 ##########2
4
2

其中,sys.getrefcount()函数可以查看一个变量的引用次数。但是需要注意,getrefcount本身也会引入一次计数

另一个要注意的是,在函数调用发生的时候会产生额外的两次引用,一次来自函数栈,另一个是函数参数

import sysa = []print(sys.getrefcount(a)) # 两次b = aprint(sys.getrefcount(a)) # 三次c = b
d = b
e = c
f = e
g = dprint(sys.getrefcount(a)) # 八次########## 输出 ##########2
3
8

需要注意,a、b、c、d、e、f、g这些变量全部指代的是同一个对象,而sys.getrefcount()函数并不是统计一个指针,而是要统计一个对象被引用的次数,所以最后一共会有八次引用

理解引用这个概念后,引用释放是一种非常自然和清晰的思想。相比C语言里需要使用free去手动释放内存,Python的垃圾回收显得更加省心省力

1.5 手动释放内存

不过,如果偏偏想手动释放内存,应该怎么做呢?

方法同样很简单,只需要先调用del a来删除对象的引用,然后强制调用gc.collect()清除没有引用的对象,即可手动启动垃圾回收

import gcshow_memory_info('initial')a = [i for i in range(10000000)]show_memory_info('after a created')del a
gc.collect()show_memory_info('finish')
print(a)########## 输出 ##########initial memory used: 48.1015625 MB
after a created memory used: 434.3828125 MB
finish memory used: 48.33203125 MB---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-12-153e15063d8a> in <module>11 12 show_memory_info('finish')
---> 13 print(a)NameError: name 'a' is not defined

到这里,是不是觉得垃圾回收非常简单呀?

但是如果此时有面试官问,引用次数为0是垃圾回收启动的充要条件吗?还有没有其他可能性呢?

二、循环引用

2.1 相互引用对象的垃圾回收

别急。不妨先思考这么一个问题:如果有两个对象它们互相引用,并且不再被别的对象所引用,那么它们应该被垃圾回收吗?

观察下面这段代码:

def func():show_memory_info('initial')a = [i for i in range(10000000)]b = [i for i in range(10000000)]show_memory_info('after a, b created')a.append(b)b.append(a)func()
show_memory_info('finished')########## 输出 ##########initial memory used: 47.984375 MB
after a, b created memory used: 822.73828125 MB
finished memory used: 821.73046875 MB

a和b互相引用,并且作为局部变量,在函数 func 调用结束后a和b这两个指针从程序意义上已经不存在了。但是,很明显依然有内存占用!为什么呢?因为互相引用导致它们的引用数都不为0

试想一下,如果这段代码出现在生产环境中,哪怕a和b一开始占用的空间不是很大,但经过长时间运行后Python所占用的内存一定会变得越来越大,最终撑爆服务器

当然,互相引用还是很容易被发现的,问题不大。可是,更隐蔽的情况是出现一个引用环,在工程代码比较复杂的情况下,引用环还真不一定能被轻易发现

那么,应该怎么做呢?

2.2 垃圾回收循环引用的变量

事实上,Python本身能够处理这种情况,可以显式调用gc.collect()来启动垃圾回收

import gcdef func():show_memory_info('initial')a = [i for i in range(10000000)]b = [i for i in range(10000000)]show_memory_info('after a, b created')a.append(b)b.append(a)func()
gc.collect()
show_memory_info('finished')########## 输出 ##########initial memory used: 49.51171875 MB
after a, b created memory used: 824.1328125 MB
finished memory used: 49.98046875 MB

所以可见,Python的垃圾回收机制并没有那么弱

2.3 垃圾回收算法

Python使用标记清除(mark-sweep)算法和分代收集(generational),来启用针对循环引用的自动垃圾回收

2.3.1 标记清除算法

先来看标记清除算法

先用图论来理解不可达的概念。对于一个有向图,如果从一个节点出发进行遍历并标记其经过的所有节点,那么在遍历结束后,所有没有被标记的节点就称之为不可达节点
显而易见,这些节点的存在是没有任何意义的,自然的需要对它们进行垃圾回收

当然,每次都遍历全图对于Python而言是一种巨大的性能浪费。所以,在Python的垃圾回收实现中,mark-sweep使用双向链表维护了一个数据结构,并且只考虑容器类的对象(只有容器类对象才有可能产生循环引用)

2.3.2 分代收集算法

分代收集算法,则是另一个优化手段
Python将所有对象分为三代,刚刚创立的对象是第 0 代,经过一次垃圾回收后依然存在的对象,便会依次从上一代挪到下一代,而每一代启动自动垃圾回收的阈值则是可以单独指定的。当垃圾回收器中新增对象减去删除对象达到相应的阈值时,就会对这一代对象启动垃圾回收

事实上,分代收集基于的思想是新生的对象更有可能被垃圾回收,而存活更久的对象也有更高的概率继续存活。因此,通过这种做法可以节约不少计算量,从而提高Python的性能

刚刚面试官的问题可以回答得上来了么?
没错,引用计数是其中最简单的实现,不过切记,引用计数并非充要条件,它只能算作充分非必要条件,至于其他的可能性,循环引用正是其中一种

三、调试内存泄漏

不过,虽然有了自动回收机制,但这也不是万能的,内存泄漏是不想见到的,但是有没有什么好的调试手段呢?

答案当然是肯定的,接下来介绍一个得力助手objgraph

objgraph,一个非常好用的可视化引用关系的包

3.1 show_refs

在这个包中主要推荐两个函数,第一个是show_refs(),它可以生成清晰的引用关系图

通过下面这段代码和生成的引用调用图,能非常直观地发现有两个list互相引用,说明这里极有可能引起内存泄露

import objgrapha = [1, 2, 3]
b = [4, 5, 6]a.append(b)
b.append(a)objgraph.show_refs([a])

3.2 show_backrefs

另一个非常有用的函数是show_backrefs(),其中的示例代码和生成图:

import objgrapha = [1, 2, 3]
b = [4, 5, 6]a.append(b)
b.append(a)objgraph.show_backrefs([a])

相比刚才的引用调用图,这张图显得稍微复杂一些。但是这个API有很多有用的参数,比如层数限制(max_depth)、宽度限制(too_many)、输出格式控制(filename output)、节点过滤(filter, extra_ignore)等,可以查阅对应的文档述

四、总结

了解Python的垃圾回收机制,主要强调下面这几点:

  1. 垃圾回收是Python自带的机制,用于自动释放不会再用到的内存空间
  2. 引用计数是其中最简单的实现,不过切记这只是充分非必要条件,因为循环引用需要通过不可达判定来确定是否可以回收
  3. Python的自动回收算法包括标记清除和分代收集,主要针对的是循环引用的垃圾收集
  4. 调试内存泄漏方面,objgraph是很好的可视化分析工具

五、思考题

  • 问题

实现一个垃圾回收判定算法,输入是一个有向图,给定起点表示程序入口点,给定有向边,输出不可达节点

  • 答案

经典的dfs(深度优先搜索)遍历,从起点开始遍历,对遍历到的节点做个记号。遍历完成后再对所有节点扫一遍,没有被做记号的就是需要垃圾回收

from typing import Setclass Graph:def __init__(self, value, nodes=None):self._value = valueself._nodes: list = [] if nodes is None else nodes@propertydef value(self):return self._value@propertydef nodes(self):return self._nodesdef node_add(self, node):self._nodes.append(node)def node_adds(self, nodes):self._nodes.extend(nodes)def node_del(self, node):self._nodes.remove(node)def __str__(self):return "Graph {} nodes {}".format(self._value, [node.value for node in self.nodes])def __repr__(self):return self.__str__()def dfs(start: Graph, includes: Set[Graph] = None) -> Set[Graph]:if includes is None:includes = set()if start in includes:return includesincludes.add(start)for s in start.nodes:includes.update(dfs(s, includes))return includesif __name__ == '__main__':A = Graph('A')B = Graph('B')C = Graph('C')D = Graph('D')E = Graph('E')F = Graph('F')has_nodes = {A, B, C, D, E, F}# A->B->E# ->C->E# B->A# D->F# F->DA.node_adds([B, C])B.node_adds([A, E])C.node_adds([E])D.node_adds([F])F.node_adds([D])graph_nodes = dfs(A, set())# OUT: {Graph B nodes ['A', 'E'], Graph E nodes [], Graph C nodes ['E'], Graph A nodes ['B', 'C']}print(graph_nodes)# OUT: {Graph F nodes ['D'], Graph D nodes ['F']}print(has_nodes - graph_nodes)

【Python核心】垃圾回收机制相关推荐

  1. 【Python知识点梳理】10.Python的垃圾回收机制、代码规范及命令行参数

    Python的垃圾回收机制.代码规范及命令行参数 文章目录 1.Python的垃圾回收机制 2.Python的引用计数机制 3.Python中的循环数据结构及引用计数 4.Python中的GC模块 5 ...

  2. python垃圾回收机制原理_详解python的垃圾回收机制

    python的垃圾回收机制 一.引子 我们定义变量会申请内存空间来存放变量的值,而内存的容量是有限的,当一个变量值没有用了(简称垃圾)就应该将其占用的内存空间给回收掉,而变量名是访问到变量值的唯一方式 ...

  3. python的垃圾回收机制和析构函数__del__

    析构函数__del__定义:在类里定义,如果不定义,Python 会在后台提供默认析构函数. 析构函数__del__调用: A.使用del 显式的调用析构函数删除对象时:del对象名: class F ...

  4. a byte of python中文版_面试官问 Python 版 “垃圾回收”机制,我没答上来

    点击"开发者技术前线",选择"星标?" 13:21 在看|星标|留言,  真爱 选自<萌萌哒的柯基> 作者:heroyfhttps://www.he ...

  5. 关于python的垃圾回收机制_Python的垃圾回收机制

    知识点的铺垫 对象和引用 python作为一门动态语言,一个简单的赋值语句也是很值得研究的,重要特点就是引用对象分离. a = 1 其中整数1是一个对象,而a是一个引用.利用赋值语句,引用a指向对象1 ...

  6. python中垃圾回收机制_python 垃圾回收机制

    首先我们要说是 以引用计数为主 标记清楚和分代回收为辅 接下来分以下几个方面解释 一 引用计数 每个对象内部都维护了一个值,该值记录这此对象被引用的次数,如果次数为0,则Python垃圾回收机制会自动 ...

  7. Python的垃圾回收机制(引用计数+标记清除+分代回收)

    一.写在前面: 我们都知道Python一种面向对象的脚本语言,对象是Python中非常重要的一个概念.在Python中数字是对象,字符串是对象,任何事物都是对象,而它们的核心就是一个结构体--PyOb ...

  8. python之垃圾回收机制

    文章目录 1.垃圾回收机制 1.1.定义 1.2.为什么要用垃圾回收机制 1.3.垃圾回收机制原理分析 1.3.1.引用计数 1.3.1.1.问题一:循环引用 1.3.1.2.问题二:效率问题 1.3 ...

  9. python内存的回收机制_简单介绍python的垃圾回收机制

    这篇文章主要介绍了python垃圾回收机制的相关资料,帮助大家更好的理解和使用python,感兴趣的朋友可以了解下 什么是垃圾回收机制? 首先,咱先来解释名词,垃圾回收是不是就是将没用的,废弃的东西回 ...

  10. python中垃圾回收机制_Python中的变量和垃圾回收机制

    1.python中的变量 python 和 java 中的变量本质不一样. java 中声明变量时要指定变量的数据类型,int.str或某一类,之后虚拟机就会在内存中申请一块空间,空间的大小跟类型相关 ...

最新文章

  1. array用法 numpy_NumPy总结(基础用法)
  2. 如何自行绕制所需要的2.2uH的电感?
  3. 加深Java基础,做了20道题选择题!简答题没做
  4. js中推断浏览器类型
  5. vivado不识别HLS生成的IP解决方法
  6. oracle那些基本知识
  7. python和财务管理的区别与联系_会计管理与财务管理区别与联系
  8. C Tricks(十九)—— 求以任意数为底的对数
  9. vb.net 教程 8-15 数据库操作实例1
  10. BPM那些事儿——BPM与SOA的演进与展望
  11. HTML制作简单个人简介页面
  12. 终于讲透了,史上最详细的RS485自动收发电路你一定要掌握
  13. Eclipse常用快捷键
  14. 剥丝抽茧|阿里面试题解读:MQ消费端遇到瓶颈该怎么办?
  15. [自学笔记]UE4(虚幻四)初学者入门
  16. 使用Clion优雅的开发嵌入式
  17. 二十一世纪大学英语读写基础教程学习笔记(原文)——10 - The Future(未来)
  18. 制作U盘启动盘重装macOS High Sierra
  19. 水果贵到吃不起,“水果自由”要电商平台来实现?
  20. INCA官方手册阅读记录

热门文章

  1. 解答:C语言中结构体的定义中,后面跟的“结构体变量”到底是什么?
  2. 数据结构题集(c语言版)第1章:绪论
  3. VS2017 winform开发知识点汇总
  4. 【Marvelous Designer 1】初学MD
  5. 环洋市场调研-2021年全球颜料红48:3行业调研及趋势分析报告
  6. 环洋市场调研-2021年全球颜料红48:2行业调研及趋势分析报告
  7. 阿里云史上最大规模降价:核心产品价格全线下调,最高降幅达50%
  8. vb microsoft html object library,HTML对象库简介(Microsoft HTML Object Library - mshtml.tlb)...
  9. 模拟房间冲击响应(Room Impulse Response,RIR),AI去混响
  10. 2023年美赛(MCM/ICM)简介