一文解密Python中的垃圾回收
作者:郝同学的测开笔记
前言
我们知道,python
是一种高级编程语言,它提供了自动内存管理的功能,即垃圾回收机制。垃圾回收机制是一种自动管理内存的技术,它可以帮助开发者在编写代码时不必关注内存的分配和释放,从而提高开发效率。好奇的同学会问了,python
的垃圾回收机制到底是如何实现的呢?带着疑问,我们一起进行探索。
为啥需要垃圾回收
- 内存泄漏:在程序运行过程中,如果开发者没有正确地释放不再使用的内存,就会导致内存泄漏。内存泄漏会导致程序占用的内存越来越多,最终可能导致程序崩溃或者系统变得非常缓慢。垃圾回收机制可以自动检测和回收不再使用的内存,避免内存泄漏的问题。
- 简化内存管理:在一些低级编程语言中,开发者需要手动分配和释放内存,这样容易出现内存泄漏和内存溢出的问题。而垃圾回收机制可以自动管理内存,开发者不需要关注内存的分配和释放,从而提高开发效率。
总之,垃圾回收的存在是为了解决内存泄漏和简化内存管理的问题。它可以自动检测和回收不再使用的内存,避免内存泄漏,并提高开发效率。在高级编程语言中,垃圾回收是一项非常重要的功能。
怎么实现的呢
Python 的垃圾回收机制主要通过引用计数和循环引用检测来实现。
引用计数
引用计数是一种简单而高效的垃圾回收算法,它通过记录每个对象的引用数量来判断对象是否仍然被使用。当一个对象的引用计数为0时,说明该对象已经不再被使用,可以被回收。接下来,我们利用sys.getrefcount()
查看变量的引用次数,这样你一定会清晰很多。
案例一
import sys a = [] print(sys.getrefcount(a)) # 2 def func(a): print(sys.getrefcount(a)) # 4 func(a) print(sys.getrefcount(a)) # 2
- 第一个
print
会输出2,有2次引用,一次来自 a,一次来自 getrefcount - 第二个
print
会输出4,有4次引用,一次来自a,一次来自python 的函数调用栈,一次来自函数参数,一次来自 getrefcount - 第三个
print
会输出2,有2次引用,一次来自a,一次来自 getrefcount
强调一点:在函数调用发生时,会产生额外的2次引用,一次来自函数栈,一次来自函数参数
案例二
我们在举个例子,加深理解
import sys a = [] print(sys.getrefcount(a)) # 2 b = a print(sys.getrefcount(a)) # 3 c = b d = b e = c f = e g = d print(sys.getrefcount(a)) # 8
可以看到a、b、c、d、e、f、g 这些变量全部指代的是同一个对象,这个对象被引用8次,所以最终输出8
案例三
我们看看,未回收和回收后内存的变化。
import os import psutil def show_memory_info(hint): pid = os.getpid() p = psutil.Process(pid) info = p.memory_full_info() memory = info.uss / 1024. / 1024 print('{} 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')
我们定义了一个函数show_memory_info
用来打印当前python
程序占用的内存大小,定义了一个函数func()
来创建变量a
,在创建变量a
之前打印占用内存,最后在函数func()
调用销毁后,再次打印内存占用。在看过案例一之后,相信你一定知道,函数内部声明的列表 a 是局部变量,在函数返回后,局部变量的引用会注销掉;此时,列表 a 所指代对象的引用数为 0,Python 便会执行垃圾回收。我们看看执行结果是不是这样:
initial memory used: 30.75 MB
after a created memory used: 415.6328125 MB
finished memory used: 30.98828125 MB
可以看到确实如此。
那我们如果将变量声明为全局变量,这样函数销毁后,列表的引用计数还存在,内存应该还是很大。测试一下:
def func(): show_memory_info('initial') global a a = [i for i in range(10000000)] show_memory_info('after a created')
执行结果如下:
initial memory used: 30.25390625 MB
after a created memory used: 415.38671875 MB
finished memory used: 415.38671875 MB
可以看到结果是满足预期的。
那如果我们将func()
函数生成的列表返回return a
,然后调用函数并赋值给一个变量,此时列表引用也会存在,内存不会释放。测试一下
def func(): show_memory_info('initial') a = [i for i in range(10000000)] show_memory_info('after a created') return a f = func()
执行结果如下:
initial memory used: 30.1875 MB
after a created memory used: 415.0703125 MB
finished memory used: 415.0703125 MB
可以看到,确实还有大量内存被占用。
到这里,应该对引用计数释放内存有一个清晰的认识了吧,现在,有人会问,我确实在某种场景下,需要手动释放内存该怎么办呢?当然python
也是支持的,还是上面定义全局变量a
的例子,我们只需要最后执行del a
,删除对象的引用,然后强制调用gc.collect()
,即可手动启动垃圾回收。
循环引用
在上面案例三中,我们提到局部变量,在函数返回后,局部变量的引用会注销掉。看下面这段例子:
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')
按照我们上面学习的,a和b都是局部变量,函数返回后,应该引用计数会变为0,内存会释放,我们测试一下:
执行结果如下:
initial memory used: 30.80078125 MB
after a, b created memory used: 801.99609375 MB
finished memory used: 801.99609375 MB
可以看到内存并没有释放,说明a
和b
的引用计数应该不为0。为啥出现这种情况呢?就是因为相互引用。那这种情况怎么解决呢?引用计数最后一部分提到,可以强制调用gc.collect()
什么是循环引用
循环引用是指两个或多个对象之间相互引用,形成一个环状结构。这种情况下,引用计数算法无法正确判断对象是否仍然被使用,因为它们的引用计数永远不会变为0。为了解决循环引用的问题,Python 引入了垃圾回收器,它使用了一种称为标记-清除的算法。
标记-清除算法分为两个阶段:标记阶段和清除阶段。在标记阶段,垃圾回收器会从根对象开始遍历所有可达对象,并将它们标记为活动对象。而在清除阶段,垃圾回收器会遍历整个堆内存,将未标记的对象进行回收。
除了引用计数和标记-清除算法,Python 还使用了分代回收的策略。分代回收是一种基于对象存活时间的优化策略,它将对象分为不同的代,每个代有不同的回收频率。一般来说,新创建的对象会被分配到第0代,而经过一次垃圾回收后仍然存活的对象会被提升到下一代。这样可以减少垃圾回收的频率,提高性能。
如何调试内存泄漏
这里推荐objgraph
,是一个可视化引用关系的包。
import objgraph a = [1, 2, 3] b = [4, 5, 6] a.append(b) b.append(a) objgraph.show_refs([a])
这里通过show_refs()
可以生成清晰的引用关系图。
更多用法,可以参考文档
最后
Python 的垃圾回收机制是一种自动管理内存的技术,它通过引用计数和循环引用检测来判断对象是否仍然被使用,并使用标记-清除算法进行回收。此外,还采用了分代回收的策略来优化性能。了解这些垃圾回收机制的工作原理对于编写高效的 Python 代码非常重要。
以上就是一文解密Python中的垃圾回收的详细内容,更多关于Python垃圾回收的资料请关注脚本之家其它相关文章!