IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Python知识库 -> 【Python核心】垃圾回收机制 -> 正文阅读

[Python知识库]【Python核心】垃圾回收机制

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

对于服务器,这种设计为永不中断的系统来说内存管理则显得更为重要,不然很容易引发内存泄漏。什么是内存泄漏呢?

  • 程序本身没有设计好,导致程序未能释放已不再使用的内存
  • 并不是内存在物理上消失,而是意味着代码在分配了某段内存后,因为设计错误失去了对这段内存的控制,从而造成了内存的浪费

那么,Python又是怎么解决这些问题的?换句话说,对于不会再用到的内存空间,Python是通过什么机制来回收这些空间的呢?

一、计数引用

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

那么,怎么知道一个对象是否永远都不能被调用了呢?

非常直观的一个想法,就是当这个对象的引用计数(指针数)为0的时候,说明这个对象永不可达,自然它也就成为了垃圾需要被回收

1.1 局部变量内存使用和释放

看一个例子:

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便会执行垃圾回收,因此占用的大量内存被释放

1.2 全局变量内存使用和释放

明白这个原理后,稍微修改一下代码:

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声明为全局变量
那么,即使函数返回后列表的引用依然存在,于是对象就不会被垃圾回收掉,依然占用大量内存

1.3 主程序引用的变量内存使用和释放

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

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

1.4 内部引用计数机制

这是最常见的几种情况
由表及里,下面深入看一下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()函数并不是统计一个指针,而是要统计一个对象被引用的次数,所以最后一共会有八次引用

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

1.5 手动释放内存

不过,如果偏偏想手动释放内存,应该怎么做呢?

方法同样很简单,只需要先调用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

到这里,是不是觉得垃圾回收非常简单呀?

但是如果此时有面试官问,引用次数为0是垃圾回收启动的充要条件吗?还有没有其他可能性呢?

二、循环引用

2.1 相互引用对象的垃圾回收

别急。不妨先思考这么一个问题:如果有两个对象它们互相引用,并且不再被别的对象所引用,那么它们应该被垃圾回收吗?

观察下面这段代码:

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

试想一下,如果这段代码出现在生产环境中,哪怕a和b一开始占用的空间不是很大,但经过长时间运行后Python所占用的内存一定会变得越来越大,最终撑爆服务器

当然,互相引用还是很容易被发现的,问题不大。可是,更隐蔽的情况是出现一个引用环,在工程代码比较复杂的情况下,引用环还真不一定能被轻易发现

那么,应该怎么做呢?

2.2 垃圾回收循环引用的变量

事实上,Python本身能够处理这种情况,可以显式调用gc.collect()来启动垃圾回收

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的垃圾回收机制并没有那么弱

2.3 垃圾回收算法

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

2.3.1 标记清除算法

先来看标记清除算法

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

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

2.3.2 分代收集算法

分代收集算法,则是另一个优化手段
Python将所有对象分为三代,刚刚创立的对象是第 0 代,经过一次垃圾回收后依然存在的对象,便会依次从上一代挪到下一代,而每一代启动自动垃圾回收的阈值则是可以单独指定的。当垃圾回收器中新增对象减去删除对象达到相应的阈值时,就会对这一代对象启动垃圾回收

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

刚刚面试官的问题可以回答得上来了么?
没错,引用计数是其中最简单的实现,不过切记,引用计数并非充要条件,它只能算作充分非必要条件,至于其他的可能性,循环引用正是其中一种

三、调试内存泄漏

不过,虽然有了自动回收机制,但这也不是万能的,内存泄漏是不想见到的,但是有没有什么好的调试手段呢?

答案当然是肯定的,接下来介绍一个得力助手objgraph

objgraph,一个非常好用的可视化引用关系的包

3.1 show_refs

在这个包中主要推荐两个函数,第一个是show_refs(),它可以生成清晰的引用关系图

通过下面这段代码和生成的引用调用图,能非常直观地发现有两个list互相引用,说明这里极有可能引起内存泄露

import objgraph

a = [1, 2, 3]
b = [4, 5, 6]

a.append(b)
b.append(a)

objgraph.show_refs([a])

在这里插入图片描述

3.2 show_backrefs

另一个非常有用的函数是show_backrefs(),其中的示例代码和生成图:

import objgraph

a = [1, 2, 3]
b = [4, 5, 6]

a.append(b)
b.append(a)

objgraph.show_backrefs([a])

在这里插入图片描述

相比刚才的引用调用图,这张图显得稍微复杂一些。但是这个API有很多有用的参数,比如层数限制(max_depth)、宽度限制(too_many)、输出格式控制(filename output)、节点过滤(filter, extra_ignore)等,可以查阅对应的文档述

四、总结

了解Python的垃圾回收机制,主要强调下面这几点:

  1. 垃圾回收是Python自带的机制,用于自动释放不会再用到的内存空间
  2. 引用计数是其中最简单的实现,不过切记这只是充分非必要条件,因为循环引用需要通过不可达判定来确定是否可以回收
  3. Python的自动回收算法包括标记清除和分代收集,主要针对的是循环引用的垃圾收集
  4. 调试内存泄漏方面,objgraph是很好的可视化分析工具

五、思考题

  • 问题

实现一个垃圾回收判定算法,输入是一个有向图,给定起点表示程序入口点,给定有向边,输出不可达节点

  • 答案

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

from typing import Set


class Graph:
    def __init__(self, value, nodes=None):
        self._value = value
        self._nodes: list = [] if nodes is None else nodes

    @property
    def value(self):
        return self._value

    @property
    def nodes(self):
        return self._nodes

    def node_add(self, node):
        self._nodes.append(node)

    def node_adds(self, nodes):
        self._nodes.extend(nodes)

    def node_del(self, node):
        self._nodes.remove(node)

    def __str__(self):
        return "Graph {} nodes {}".format(self._value, [node.value for node in self.nodes])

    def __repr__(self):
        return self.__str__()


def dfs(start: Graph, includes: Set[Graph] = None) -> Set[Graph]:
    if includes is None:
        includes = set()
    if start in includes:
        return includes
    includes.add(start)
    for s in start.nodes:
        includes.update(dfs(s, includes))
    return includes


if __name__ == '__main__':
    A = Graph('A')
    B = Graph('B')
    C = Graph('C')
    D = Graph('D')
    E = Graph('E')
    F = Graph('F')
    has_nodes = {A, B, C, D, E, F}

    # A->B->E
    # ->C->E
    # B->A
    # D->F
    # F->D
    A.node_adds([B, C])
    B.node_adds([A, E])
    C.node_adds([E])
    D.node_adds([F])
    F.node_adds([D])
    graph_nodes = dfs(A, set())
    # OUT: {Graph B nodes ['A', 'E'], Graph E nodes [], Graph C nodes ['E'], Graph A nodes ['B', 'C']}
    print(graph_nodes)
    # OUT: {Graph F nodes ['D'], Graph D nodes ['F']}
    print(has_nodes - graph_nodes)
  Python知识库 最新文章
Python中String模块
【Python】 14-CVS文件操作
python的panda库读写文件
使用Nordic的nrf52840实现蓝牙DFU过程
【Python学习记录】numpy数组用法整理
Python学习笔记
python字符串和列表
python如何从txt文件中解析出有效的数据
Python编程从入门到实践自学/3.1-3.2
python变量
上一篇文章      下一篇文章      查看所有文章
加:2021-07-26 12:02:42  更:2021-07-26 12:03:01 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/25 15:11:01-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计