python 基础知识梳理——Python中的垃圾回收机制

python 基础知识梳理——Python中的垃圾回收机制


1. 引言

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

泄漏指的是:程序本身没有设计好,导致程序本身未能释放已经不再使用的内存

内存泄漏指的是:在代码分配某段内存后,因为设计错误,失去了对这段内存的控制,从而造成的内存的浪费。

我们只需要记住最关键的一句话:

Python中的垃圾回收机制是以引用计数为主,标记-清除和分代回收两种机制为辅的策略

2. 引用计数

Python中一切皆是对象,所以我们看到的所有的变量,本质上都是对象的一个指针。

那么,我们怎么判断这个对象是否需要被回收呢?

当这个对象的引用计数(指针数)为0,那么就意味着没有对于这个对象的引用,自然这个对象就成了垃圾,需要被回收。

2.1 例1:a为局部变量

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. / 1024
    print('{} memroy used:{} MB'.format(hint,memory))

def func():
    show_memory_info('initial')
    a = [i for i in range(10000000)]
    show_memory_info('after created ')

func()
show_memory_info('finished')
# 输出
initial memroy used:6.1796875 MB
after created  memroy used:398.26953125 MB
finished memroy used:10.515625 MB

在调用func()后,在列表a创建后,内存占用达到了近400MB,而在函数调用结束后,内存则恢复到了之前的水平。

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

2.2 例2:a为全局变量

那么,我们将a声明为全局变量会发生什么呢?

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. / 1024
    print('{} memroy used:{} MB'.format(hint,memory))

def func():
    show_memory_info('initial')
    global a # 声明全局变量a
    a = [i for i in range(10000000)]
    show_memory_info('after created ')

func()
show_memory_info('finished')
# 输出
initial memroy used:6.16796875 MB
after created  memroy used:398.28515625 MB
finished memroy used:398.28515625 MB

在我们声明a为全局变量后,即使函数返回后,列表的引用依然存在,此时Python的垃圾回收机制就不会回收a,依然占用内存。

2.3 例子3:a作为返回值

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. / 1024
    print('{} memroy used:{} MB'.format(hint,memory))

def func():
    show_memory_info('initial')
    a = [i for i in range(10000000)]
    show_memory_info('after created ')
    return a

a = func()
show_memory_info('finished')
# 输出
initial memroy used:6.1640625 MB
after created  memroy used:398.2578125 MB
finished memroy used:398.2578125 MB

a作为返回值的话,因为在a=fun()中引用了a,所以a还是不会被垃圾回收机制回收,内存仍然被占用着。

2.4 引用计数原理

import sys

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

sys.getrefcount()这个函数,可以查看一个变量的引用次数。

注意:getrefcount() 本身也会引用一次计数。

注意:当函数发生的时候,会产生额外两次的引用,一次来自函数栈,另一次来自函数参数。

import sys
a = []
# 两次引用计数,一次是a,另一次是sys.getrefcount()
print('引用计数为:{} 次'.format(sys.getrefcount(a)))

b = a
# 三次 引用计数,前两次 + b=a的一次引用,sys.getrefcount()重复只计算一次
print('引用计数为:{} 次'.format(sys.getrefcount(a)))

c = b # 四次
d = b # 五次
e = b # 六次
f = b # 七次
g = d # 八次,通过 d 引用了 b
print('引用计数为:{} 次'.format(sys.getrefcount(a)))

2.5 手动垃圾回收

Python中垃圾回收相比于C语言中的free来释放内存,简单了很多,但是如果我们需要手动释放内存呢?该如何操作呢?

那么,我们就需要先调用del a来删除一个对象,然后调用gc.collect()启动垃圾回收,代码如下:

import gc
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('{} memroy used:{} MB'.format(hint, memory))

show_memory_info('initial')
a = [i for i in range(10000000)]
show_memory_info('after created')
del a
gc.collect()
show_memory_info('finished')
print(a)
# 输出
initial memroy used:6.16015625 MB
after created memroy used:398.21875 MB
finished memroy used:10.46875 MB 
Traceback (most recent call last):
  File "/Users/gray/Desktop/test.py", line 18, in <module>
    print(a)
NameError: name 'a' is not defined

可见,这就是Python的手动回收机制,经过手动回收,内存空间被回收了,列表a也被删除,所以报错显示a没有定义。

3. 循环引用

我们知道了引用计数为0,Python就会回收,那么如果两个对象互相引用,那么它们会被垃圾回收吗?

我们从一个例子入手:

import gc
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('{} memroy used:{} MB'.format(hint, memory))

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 memroy used:6.21484375 MB
after a,b created memroy used:660.42578125 MB
finished memroy used:660.15625 MB

很明显,由于a和b之间的互相引用,即使a,b均为局部变量,在函数调用结束之后,a和b的指针在程序意义上都不存在了,但是内存依然占用。

在这样简单的代码中,我们还是能够发现循环引用的,但是当工程代码复杂后,引用环不一定会容易被轻易发现。

循环引用这种情况,Python也是可以处理的,我们还是调用gc.collect()来手动启动垃圾回收。

import gc
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('{} memroy used:{} MB'.format(hint, memory))

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()
gc.collect()
show_memory_info('finished')
# 输出
initial memroy used:6.15234375 MB
after a,b created memroy used:784.7109375 MB
finished memroy used:10.74609375 MB

手动垃圾回收生效了,Python中的垃圾回收并没有那么弱。

4. Python的垃圾回收

针对循环引用,Python中有专门的标记-清除分代回收来处理。

4.1 标记-清除

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

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

4.2 分代回收

Python将所有对象分为三代。刚刚创建的对象为0代;经历一次垃圾回收后,依然存在的对象,便会依次从上一代挪到下一代;每一代启动垃圾回收的阈值,是可以单独设定的。当垃圾回收容器中新增对象减去删除对象达到相应的阈值的时候,就会对这一代对象启动垃圾回收。

分代回收的思想其实是:新生代对象更有可能会被回收,而存活更久的对象也有更高的概率继续存活,通过这样的思路,可以节省不少计算量,从而提高Python的性能。






博文的后续更新,请关注我的个人博客:星尘博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值