今天要讲的是Python的垃圾回收机制

众所周知,我们现在的计算机都是图灵架构。图灵架构的本质,就是一条无限长的纸带,对应着我们的存储器。随着寄存器、异失性存储器(内存)和永久性存储器(硬盘)的出现,也出现了一个矛盾——存储器越来越快,价格也越来越贵。因此,如何利用好每一份告诉存储器的控件,永远是系统设计的一个核心。

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

而对于服务器来说,这种设计对于不中断的系统哦过来说,内存管理就显得尤为重要,不然很容易引发内存泄漏的现象。

什么是内存泄漏?

这里的泄漏,并不是说内存出现了信息安全的问题,被恶意程序利用了,而是指程序没有设计好,导致程序未能释放已经不再使用的内存

内存泄漏也不是指内存在物理上消失了,而是意味着代码在分配了某段内存后,因为设计失误,市区了对这块内存的控制,从而导致了内存资源的浪费。

那么,Python优势如何解决这些问题的呢?更明确的问题:对于不会再次用到的内存空间,Python又是通过什么机制来回收的呢?

计数引用

我们在前面不停的强调过,Python中一切皆为对象,因此,我们所有的一切变量,本质上都是对象的一个指针,那么如何知道一个对象,是否永远都不被调用了呢?

我们在上一章讲的一个非常直观的思路,就是当这个对象的引用计数(类似于指针)为0的时候,说明这个对象用不可达,呢么这个时候,它也就自然成为了垃圾,需要被回收。

我们这时候看看下面的例子:

import os
import psutil def show_memory_info(hint):
pid = os.getpid()
p = psutil.Process(pid) info = p.memory_full_info
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(100000000)]
show_memory_info('after a created') func()
show_memory_info('finished')

通过这个例子,我们可以看出来,在调用甘薯func后,列表a被创建,内存就会占用比较多,但是在函数调用以后内存则返回正常。

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

然后我们把代码稍微修改一下(我们只改动func函数)

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

我们在上面的代码里,把a声明为全局变量,那么即使函数返回以后,垃圾回收就不会被触发,大量的内存仍然被占用。或者下面的方式也是一样的

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

这里,函数通过返回值,生成的列表依旧是被引用的,所以垃圾回收也没被触发。

上面就是最常见的几种情况。由表及里,下面,我们深入看一下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))

这里我们引入一个新的函数

sys.getrefcount()

这个函数,是可以查看一个变量的引用次数。这段代码本身应该很好理解,但是,getrefcount本身也会引入一次计数。

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

import sys

a = []

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这些变量指的是同一个变量,而sys.getrefcount()并不是统计一个指针,而是要统计一个对象被引用的次数,所以最后会有8次引用。

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

可是,如果我们想用手动的方式释放内存,又该怎么操作呢?

其实我们首先用del来删除对象的引用,然后强制调用gc.collect()清除没有引用的对象,就可以手动启动垃圾回收。

import sys
import gc a = [i for i in range(100000000)] del a
gc.collect()

按照上面的方法就实现了手动的垃圾回收。

这里可以考虑一个问题:

引用次数为0是垃圾回收启动的充要条件么?

我们可以一步一步的看:先看看下面的代码

def fun():
show_memory_info('initial')
a = [i for i in range(100000000)]
b = [i for i in range(100000000)]
show_memory_info('after a,b created')
a.append(b)
b.append(a) fun()
show_memory_info('finish')

在上面的程序段里,a和b列表互相引用,并且是作为局部变量的,但是在函数fun调用以后,a和b的指针从程序意义上已经不存在了,但是很明显的,依然有内存占用!这是为什么呢?因为互相引用,导致他们的引用数都不为0

再想一想,如果这段代码实在实际生产环境中,即便是a和b开始的时候占用的空间没有很大,但是经过长时间的运行以后,Python所占用的内存会原来越大。最终服务器就爆掉了,后果不堪设想。

虽然在很多的环境下互相引用很容易被发现,问题不会特别大,但是更隐蔽的情况是一个引用环的出现,在工程代码比较复杂的情况下,引用环是很不容易被发现的。那我们又该怎么办呢?这种情况下,我们就需要我们前面所讲的,显式的调用gc.collect()来启动垃圾回收。

import gc
def fun():
show_memory_info('initial')
a = [i for i in range(100000000)]
b = [i for i in range(100000000)]
show_memory_info('after a,b created')
a.append(b)
b.append(a) fun()
gc.collect()
show_memory_info('finish')

Python使用标记清除(mark-sweep)算法和分代收集(generational),来针对循环引用的自动垃圾回收,我们在这里还可以简单的介绍一下

标记清除算法

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

但是,每次遍历全图对Python而言是一种巨大的性能浪费,所以,在Python的垃圾回收实现中,mark-sweep使用双向链表维护一个数据结构,并且只考虑容器类的对象(只有容器类对象才能产生循环引用)。具体的算法我们这里就不讲了,只是看看大概的实现思路是什么

而分代收集算法,则是另一个优化手段

Python讲所有的对象都分为3代,刚刚创立的对象是第0代,经历过一次垃圾回收的对象,变回依次从上一代挪到下一代。而每一代启动自动垃圾回收的阈值,则是可以单独指定的。当垃圾回收容器中新增对象减去删除对象达到相应的阈值时,就会对这一代对象启动垃圾回收。

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

回到刚才的那个问题,引用计数是其中最简单的实现,不过引用计数并非其充要条件,他只能作为充分不必要条件;至于其他的可能性,我们所讲的循环引用正式其中一种。

调试内存泄漏

即便是有了自动回收机制,但切记这也不是万能的。内存泄漏是我们不想见到的十分影响性能的。有没有什么调试的手段呢?下面我们就来介绍以为十分得力的助手一名——objgraph,他是一个非常好用的可视化饮用哦过关系的包,这里就主要推荐两个函数——show_refs(),他可以生成清晰的引用关系图(objgrph可以通过pip安装,代码会生成一个.doc的文件,可以用graphviz打开,官网链接,或者直接从网盘上下载(提取码73z4)。软件在解压后bin文件夹内的GVEdit.exe。

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

打开生成的图片

可以看出来生成的上面那段代码的引用调用图,很直观的发现,有两个list互相引用,说明很容易引起内存泄漏。这样就很容易去排插代码层。

另一个非常有用的函数是show_backrefs(),我们还用上面的两个列表来展示一下:

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

再看一下生成的图片

这个图就稍微复杂了一些,但是这个API内包含了更多的参数,我们在使用之前可以了解一下他的官方文档

总结

最后我们来总结一下这一章节的内容

1.垃圾回收是Python自带的机制,用于释放不会再用到的内存空间;

2.引用计数是其中最简单的实现方法,不过要注意,他只是个充分非必要条件,因为循环引用需要通过不可达判定释放可以回收;

3.Python的自动回收算法包括标记清除和分代收集,主要针对的是循环引用的垃圾收集;

4.调试内存泄漏可以用objgraph这个可视化的分析工具。

课后思考

自己如何实现一个垃圾回收的判定方法呢?要求比较简单:输入一个有向图,给定起点表示程序入口点,给定有向边,输出不可达节点。

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

Python核心技术与实战——二十|Python的垃圾回收机制的更多相关文章

  1. Python核心技术与实战——二十|assert的合理利用

    我们平时在看代码的时候,或多或少会看到过assert的存在,并且在有些code review也可以通过增加assert来使代码更加健壮.但是即便如此,assert还是很容易被人忽略,可是这个很不起眼的 ...

  2. 编程语言和python介绍, 变量,小整数池,垃圾回收机制

    1.编程语言的发展史 计算机是基于电工作(基于高.低电平)1010010101011 1.机器语言 优点:执行速度够快 缺点:开发效率非常低 2.汇编语言(通过英文字符组成) 优点:执行效率相较于机器 ...

  3. 初识JVM:(二)Java的垃圾回收机制详解

    声明:本文主要参考https://www.cnblogs.com/codeobj/p/12021041.html 仅供个人学习.研究之用,请勿用于商业用途,如涉及侵权,请及时反馈,立刻删除. 一.Ja ...

  4. Python核心技术与实战——二一|巧用上下文管理器和with语句精简代码

    我们在Python中对于with的语句应该是不陌生的,特别是在文件的输入输出操作中,那在具体的使用过程中,是有什么引伸的含义呢?与之密切相关的上下文管理器(context manager)又是什么呢? ...

  5. (65)Wangdao.com第十天_JavaScript 垃圾回收机制 GC

    垃圾积累过多,致使程序运行缓慢,什么是垃圾? 当堆中某个内容,再也没有指针指向它,我们将再也用不了它,此时就是一个垃圾. 出现这种情况是因为 obj = null; 此时,js 中的垃圾回收机制会自动 ...

  6. 详解python的垃圾回收机制

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

  7. python入门之垃圾回收机制

    目录 一 引入 二.什么是垃圾回收机制? 三.为什么要用垃圾回收机制? 四.垃圾回收机制原理分析 4.1.什么是引用计数? 4.2.引用计数扩展阅读 4.2.1 标记-清除 4.2.2 分代回收 一 ...

  8. Python语法之垃圾回收机制

    目录 一 引入 二.什么是垃圾回收机制? 三.为什么要用垃圾回收机制? 四.垃圾回收机制原理分析 4.1.什么是引用计数? 4.2.引用计数扩展阅读 一 引入 解释器在执行到定义变量的语法时,会申请内 ...

  9. 6、Python语法之垃圾回收机制

    一 .引入 解释器在执行到定义变量的语法时,会申请内存空间来存放变量的值,而内存的容量是有限的,这就涉及到变量值所占用内存空间的回收问题,当一个变量值没有用了(简称垃圾)就应该将其占用的内存给回收掉, ...

随机推荐

  1. C# 跨线程更新 UI

    Winforms 跨线程更新 UI 在 Winforms 中, 所有的控件都包含 InvokeRequired 属性, 如果我们要更新UI,通过它我们可以判断是否需要调用 [Begin]Invoke. ...

  2. 安装 Git 并连接 Github

    下载安装 Git, 下载地址:https://git-scm.com/download/win . 在命令行中输入 git 测试 Git 是否安装成功. 在桌面鼠标右击打开 Git Bash Here ...

  3. PI膜应变片试样制备

    一.选取基板 1.喷涂在玻璃基板上PI膜 2.正面用记号笔标记PI膜工艺参数——转速.厚度 3.玻璃板背面为PI膜 二.贴防护膜 1.事先画好二维图,以dxf格式存放 2.裁减合适的大小,并将其贴在打 ...

  4. OpenStack组件——Glance镜像服务

    1.glance介绍 Glance是Openstack项目中负责镜像管理的模块,其功能包括虚拟机镜像的查找.注册和检索等. Glance提供Restful API可以查询虚拟机镜像的metadata及 ...

  5. VMware Workstation 15 Pro简化安装Kali Linux 2019.2

    记录下简单安装的步骤

  6. 继续做一道linux的企业 面试题

    把/dongdaxia目录及其子目录小所有以拓展名.sh结尾的文件中包含dongdaxia的字符串全部替换为dj. 解答:这道题还是用到了三剑客里的sed: 第一步:先在/dongdaxia目录及其子 ...

  7. python函数 全局变量和局部变量

    li1=[1,2,3,4,5] str1='abc' def func1(): li1=[7,8,9] str1='efg' print(str1) func1() print(li1)#输出的结果为 ...

  8. Go语言中的数组(九)

    我刚接触go语言的数组时,有点不习惯,因为相对于JavaScript这样的动态语言里的数组,go语言的数组写起来有点不爽. 定义数组 go语言定义数组的格式如下: ]int var 数组名 [数组长度 ...

  9. Spring添加声明式事务

    一.前言 Spring提供了声明式事务处理机制,它基于AOP实现,无须编写任何事务管理代码,所有的工作全在配置文件中完成. 二.声明式事务的XML配置方式 为业务方法配置事务切面,需要用到tx和aop ...

  10. C++多线程基础学习笔记(六)

    condition_variable.wait.notifiy_one.notify_all的使用方式 condition_variable:条件变量 wait:等待被唤醒 notify_one:随机 ...