python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python垃圾回收

一文解密Python中的垃圾回收

作者:郝同学的测开笔记

我们知道,python 是一种高级编程语言,它提供了自动内存管理的功能,即垃圾回收机制,所以本文就来聊聊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
​

强调一点:在函数调用发生时,会产生额外的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

可以看到内存并没有释放,说明ab的引用计数应该不为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垃圾回收的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文