zoukankan      html  css  js  c++  java
  • Python核心技术与实战——二十|Python的垃圾回收机制

    今天要讲的是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)遍历,从起点处开始遍历,对遍历到的节点做一个记号,遍历完成后对所有的节点扫一遍,没有被做记号的,就是需要垃圾回收。

  • 相关阅读:
    【HDOJ】2267 How Many People Can Survive
    【HDOJ】2268 How To Use The Car
    【HDOJ】2266 How Many Equations Can You Find
    【POJ】2278 DNA Sequence
    【ZOJ】3430 Detect the Virus
    【HDOJ】2896 病毒侵袭
    求奇数的乘积
    平方和与立方和
    求数列的和
    水仙花数
  • 原文地址:https://www.cnblogs.com/yinsedeyinse/p/11949475.html
Copyright © 2011-2022 走看看