Python核心丨垃圾回收机制

Python垃圾回收机制


计数引用

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

如何知道一个对象,是否永远都不能被调用了?

可以通过这个对象的引用计数(指针数)为0的时候,说明这个对象永不可达,自然它就称为了垃圾,需要被回收。

示例

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('{} 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便会执行垃圾回收,因此之前占用的大量内存就又回来了。

修改后的代码

def func():
    show_memory_info('initial')
    global a
    a = [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声明为全局变量。那么,即使函数返回后,列表的引用依然存在,于是对象就不会被垃圾回收掉,依然占用大量内存。

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

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

a = 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

深入了解Python内部的引用计数机制

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 = []

print(sys.getrefcount(a)) # 两次

b = a

print(sys.getrefcount(a)) # 三次

c = b
d = b
e = c
f = e
g = d

print(sys.getrefcount(a)) # 八次

########## 输出 ##########

2
3
8

a、b、c、d、e、f、g这些变量全部指代的是同一个对象,而sys.getrefcount()函数并不是统计一个指针,而是要统计一个对象被引用的次数。

手动释放内存

只需先调用del a来删除对象的引用;然后强制调用gc.collect(),清除没有引用的对象,即可手动启动垃圾回收。

import gc

show_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
循环引用

实例

  • 如果有两个对象,它们互相引用,并且不再被别的对象所引用

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。

事实上,Python本身能够处理这种情况。

import gc

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 memory used: 49.51171875 MB
after a, b created memory used: 824.1328125 MB
finished memory used: 49.98046875 MB

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

标记清除算法

对于一个有向图,如果从一个节点出发进行遍历,并标记其经过的所有节点;那么,再遍历结束后,所有没有被标记的节点,称之为不可达节点。

显然,这些节点的存在是没有任何意义的,自然的,就需要对它们进行垃圾回收。

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

分代收集算法

Python将所有对象分为三代。刚刚创立的对象是第0代;经过一次垃圾回收后,依然存在的对象,便会依次从上一代挪到下一代。而每一代启动自动垃圾回收的阈值,则是可以单独指定的。

当垃圾回收器中新增对象减去删除对象达到响应的阈值时,就会对这一代对象启动垃圾回收。

分代收集基于的思想是,新生的对象更优可能被垃圾回收,而存活更久的对象也有更高的概率继续存活。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值