Python垃圾回收是怎么实现的

目录
  • 什么是垃圾回收
  • Python中的垃圾回收机制
    • 引用计数
      • 循环引用
    • 标记清除解除循环引用
    • 分代回收
  • 总结

什么是垃圾回收

垃圾回收(GC) 大家应该多多少少都了解过,什么是垃圾回收呢?垃圾回收GC的全拼是 Garbage Collection,在维基百科的定义是:在计算机科学中,垃圾回收(英语:Garbage Collection,缩写为GC)是一种自动的内存管理机制。当一个电脑上的动态内存不再需要时,就应该予以释放,以让出内存,这种内存资源管理,称为垃圾回收。我们都知道在C/C++里用户需要自己管理维护内存,自己管理内存是很自由,可以随意申请、释放内存,但是极易会出现内存泄露,悬空指针等问题;像现在的高级语言Java,Python等,都采用了垃圾回收机制,自动进行内存管理,而垃圾回收机制专注于两件事:① 找到内存中无用的垃圾资源。 ② 清除这些垃圾资源并把内存让出来给其他对象使用。

Python作为一门解释型语言,因为简单易懂的语法,我们可以直接对变量赋值,而不必声明变量的类型,变量类型的确定、内存空间的分配与释放都是由Python解释器在运行时自动进行的,我们不必关心;Python这一自动管理内存的功能极大的减少了开发者的编码负担,让开发者专注于业务实现,这也是成就Python自身的重要原因之一。接下来,我们就扒一扒Python的内存管理。

Python中的垃圾回收机制

引用计数

Python中一切皆对象,也就是说,在Python中你用到的一切变量,本质上都是类对象。实际上每一个对象的核心就是一个**「结构体PyObject」**,它的内部有一个引用计数器ob_refcnt,程序在运行的过程中会实时的更新ob_refcnt的值,来反映引用当前对象的名称数量。当某对象的引用计数值为0,说明这个对象变成了垃圾,那么它会被回收掉,它所用的内存也会被立即释放掉。

typedef struct _object {
    int ob_refcnt;//引用计数
    struct _typeobject *ob_type;
} PyObject;

以下情况是导致引用计数加一的情况:
①对象被创建,例如a=5
②对象被引用,b=a
③对象被作为参数,传入到一个函数中(要注意的是,在函数调用发生的时候,会产生额外的两次引用,一次来自函数栈,另一个是函数参数)
④对象作为一个元素,存储在容器中(例如存储在列表中)

下面的情况则会导致引用计数减一:
①对象别名被显示销毁 del a
②对象别名被赋予新的对象
③一个对象离开它的作用域
④对象所在的容器被销毁或者是从容器中删除对象

我们还可以通过sys包中的getrefcount()来获取一个名称所引用的对象当前的引用计数(注意,这里getrefcount()本身会使得引用计数加一)

import sys
a = [1, 2, 3]
print(sys.getrefcount(a))
# 输出为2,说明有两次引用(一次来自a的定义,一次来自getrefcount)

def func(a):
    print(sys.getrefcount(a))
    # 输出为4,说明有四次引用(a的定义、Python的函数调用栈,函数参数,和getrefcount)

func(a)
print(sys.getrefcount(a))
# 输出为2,说明有两次引用(一次来自a的定义,一次来自getrefcount),此时函数func调用已经不存在

下面从使用内存的角度看一下:

import os
import psutil

def show_memory_info(hint):
    """
    显示当前 python 程序占用的内存大小
    :param hint:
    :return:
    """
    pid = os.getpid()
    p = psutil.Process(pid)

    info = p.memory_full_info()
    memory = info.rss / 1024 / 1024
    print('{} 当前进程的内存使用: {} MB'.format(hint, memory))

def func():
    show_memory_info('初始')
    a = [i for i in range(9999999)]
    show_memory_info('创建a之后')

func()
show_memory_info('结束')

输出如下:

初始 当前进程的内存使用: 12.125 MB
创建a之后 当前进程的内存使用: 205.15625 MB
结束 当前进程的内存使用: 12.87890625 MB

可以看出,当前进程初始的内存使用为12.125 MB,当调用了函数func()创建列表a之后,内存占用迅速增加到了205.15625 MB,而在函数调用结束后,内存则返回正常。这是因为,函数内部声明的列表a是局部变量,在函数返回后,局部变量的引用会注销掉,此时列表a所指代对象的引用计数为0,Python 便会执行垃圾回收,因此之前占用的大量内存就又回来了。

循环引用

何为循环引用?简单来说就是两个对象相互引用。看下面一段程序:

def func2():
    show_memory_info('初始')
    a = [i for i in range(10000000)]
    b = [x for x in range(10000001, 20000000)]
    a.append(b)
    b.append(a)
    show_memory_info('创建a,b之后')

func2()
show_memory_info('结束')

输出如下:

初始 当前进程的内存使用: 12.14453125 MB
创建a,b之后 当前进程的内存使用: 396.6875 MB
结束 当前进程的内存使用: 396.96875 MB

可以看出,在程序中,a和b互相引用,并且作为局部变量在函数func2调用结束后,a和b从程序意义上已经不存在,但从输出结果中看到,依然有内存占用,这是为什么呢?因为互相引用导致它们的引用数都不为0。

如果在生产环境下出现了循环引用,又没有其他垃圾回收机制的情况下,经过长时间运行后,程序所占用的内存一定会变得越来越大,如果没有被及时处理,一定会跑满服务器的。

如果不得不使用循环引用的话,我们可以显式调用gc.collect() 来启动垃圾回收:

def func2():
    show_memory_info('初始')
    a = [i for i in range(10000000)]
    b = [x for x in range(10000001, 20000000)]
    a.append(b)
    b.append(a)
    show_memory_info('创建a,b之后')

func2()
gc.collect()
show_memory_info('结束')

输出如下:

初始 当前进程的内存使用: 12.29296875 MB
创建a,b之后 当前进程的内存使用: 396.69140625 MB
结束 当前进程的内存使用: 12.95703125 MB

引用计数机制有高效、简单、实时性(一旦为零就直接做掉)等优点,一旦一个对象的引用计数归零,内存就直接释放了。不用像其他机制等到特定时机。将垃圾回收随机分配到运行的阶段,处理回收内存的时间分摊到了平时,正常程序的运行比较平稳。但是,引用计数也存在着一些缺点,通常的缺点有:

① 逻辑虽然简单,但维护起来有些麻烦。每个对象需要分配单独的空间来统计引用计数,并且需要对引用计数进行维护,这是需要消耗一下资源的。
② 循环引用。这将是引用计数机制的致命伤,引用计数对此是无解的,因此必须要使用其它的垃圾回收算法对其进行补充。

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

标记清除解除循环引用

Python采用了 标记-清除(Mark and Sweep)算法,解决容器对象可能产生的循环引用问题。(注意,只有容器类对象才有可能产生循环引用,比如列表、字典、用户自定义类的对象、元组等。而像数字,字符串这类简单类型不会出现循环引用。作为一种优化策略,对于只包含简单类型的元组也不在标记清除算法的考虑之列)

它分为两个阶段:第一阶段是标记阶段,GC会把所有的活动对象打上标记,第二阶段是把那些没有标记的非活动对象进行回收。

那么Python又是如何判断什么样的对象为非活动对象的呢?

对于任何对象集合,我们先建个引用计数副本表,来存它们的引用计数,然后把集合内部的引用都解除掉(内部引用是指这个集合中的某个对象引用了本集合内部的另一个对象),解除的过程中在副本表减少引用计数,解除掉所有的内部引用后,在副本表引用计数依然不为0的,就是根集合,然后开始标记过程,即从跟集合节点逐步恢复引用并增加副本表的引用计数,最后副本表中引用计数为0的,就是垃圾对象了,我们就需要对它们进行垃圾回收。例如:

上面这个集合中的节点有外部进来的连接(到a和到b),也有到外部的连接(c引用了外面某个对象),右边是引用计数表,然后我们拆掉所有内部连接:

那么根集合就是a和b了,然后我们从a和b出发开始标记并恢复引用计数:

从a和b出发可达的节点都被恢复了,引用计数还是0的就是这个集合内部循环引用的垃圾(e和f),如果把所有对象看做一个集合,那么可以回收所有垃圾,也可以将所有对象划分成一个个小的集合,分别回收小集合内的垃圾。
但是每次都需要遍历图,对于Python而言是一种巨大的性能浪费。

分代回收

分代回收是一种以空间换时间的操作方式,Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了3代,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代)。它们对应3个链表,它们的垃圾收集频率随对象的存活时间的增大而减小。

新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,即当垃圾回收器中新增对象减去删除对象达到相应的阈值时,就会对这一代对象启动垃圾回收,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。同时,分代回收是建立在标记清除技术基础之上。事实上,分代回收基于的思想是,新生的对象更有可能被垃圾回收,而存活更久的对象也有更高的概率继续存活。因此,通过这种做法,可以节约不少计算量,从而提高Python的性能。

总结

垃圾回收是Python自带的机制,用于自动释放不会再用到的内存空间,在Python中,主要通过引用计数进行垃圾回收,通过标记清除解决容器对象可能产生的循环引用问题,通过分代回收以空间换时间的方法提高垃圾回收效率。

到此这篇关于Python垃圾回收是怎么实现的的文章就介绍到这了,更多相关Python垃圾回收内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详细分析Python垃圾回收机制

    引入 为什么要有垃圾回收机制 Python中的垃圾回收机制简称(GC),我们在程序的运行中会产生大量的变量用于保存数据,而有时候有些变量已经没有用了就需要被清理释放掉该变量所占据的内存空间.在一些较为低级的语言中(比如:C语言,汇编语言)对于内存空间的释放是需要编程人员来手动进行的,这种与底层硬件直接打交道的操作是十分的危险与繁琐的,而基于C语言开发而来的Python为了解决掉这种顾虑则自带了一种垃圾回收机制,从而让开发人员不必过分担心内存的使用情况而可以全身心的投入到开发中去. >>>

  • 理解Python垃圾回收机制

    一.垃圾回收机制 Python中的垃圾回收是以引用计数为主,分代收集为辅.引用计数的缺陷是循环引用的问题. 在Python中,如果一个对象的引用数为0,Python虚拟机就会回收这个对象的内存. #encoding=utf-8 __author__ = 'kevinlu1010@qq.com' class ClassA(): def __init__(self): print 'object born,id:%s'%str(hex(id(self))) def __del__(self): pr

  • python的内存管理和垃圾回收机制详解

    简单来说python的内存管理机制有三种 1)引用计数 2)垃圾回收 3)内存池 接下来我们来详细讲解这三种管理机制 1,引用计数: 引用计数是一种非常高效的内存管理手段,当一个pyhton对象被引用时其引用计数增加1,当其不再被引用时引用计数减1,当引用计数等于0的时候,对象就被删除了. 2,垃圾回收(这是一个很重要知识点): ①  引用计数 引用计数也是一种垃圾回收机制,而且是一种最直观,最简单的垃圾回收技术. 在Python中每一个对象的核心就是一个结构体PyObject,它的内部有一个引

  • 浅谈Python的垃圾回收机制

    一.垃圾回收机制 Python中的垃圾回收是以引用计数为主,分代收集为辅.引用计数的缺陷是循环引用的问题. 在Python中,如果一个对象的引用数为0,Python虚拟机就会回收这个对象的内存. #encoding=utf-8 __author__ = 'kevinlu1010@qq.com' class ClassA(): def __init__(self): print 'object born,id:%s'%str(hex(id(self))) def __del__(self): pr

  • python对象销毁实例(垃圾回收)

    我就废话不多说了,直接上代码吧! '''python对象销毁(垃圾回收)''' class Point: 'info class' def __init__(self,x=0,y=0): self.x = x self.y = y def __del__(self): class_name = self.__class__.__name__ print(class_name, '销毁') pt1 = Point() pt2 = pt1 pt3 = pt2 print(id(pt1),id(pt2

  • python垃圾回收机制(GC)原理解析

    这篇文章主要介绍了python垃圾回收机制(GC)原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 今天想跟大家分享的是关于python的垃圾回收机制,虽然本人这会对该机制没有很深入的了解, 但是本着热爱分享的原则,还是囫囵吞枣地坐下记录分享吧, 万一分享的过程中开窍了呢.哈哈哈. 首先还是做一下概述吧: 我们都知道, 在做python的语言编程中, 相较于java, c++, 我们似乎很少去考虑到去做垃圾回收,内存释放的工作, 其实是p

  • Python的垃圾回收机制详解

    引用计数 在Python源码中,每一个对象都是一个结构体表示,都有一个计数字段. typedef struct_object { int ob_refcnt; struct_typeobject *ob_type; } PyObject; PyObject是每个对象必有的内容,其中ob_refcnt就是作为引用计数.当一个对象有了新的引用时,它的ob_refcnt就会增加,引用它的对象被删除时则减少.一旦对象的引用计数为0,该对象立即被回收,占用空间就会被释放. 优点 简单易用 实时性好,一旦没

  • python中的垃圾回收(GC)机制

    一.引用计数 Python 垃圾回收以引用计数为主,分代回收为辅.引用计数法的原理是每个对象维护一个ob_refcnt,用来记录对象被引用的次数,也就是用来追踪有多少个引用指向了对象,当发生以下四种情况的时候,对象的引用计数+1: 对象被创建,比如:a = 14 对象被引用,比如: b = a 对象被作为参数,传给函数,比如:func(a) 对象作为容器中的一个元素,比如:List = {a, "a" , "b", 2} 与上述情况相对应,当发生以下四种情况时,对象

  • Python中垃圾回收和del语句详解

    Python中的垃圾回收算法是采用引用计数, 当一个对象的引用计数为0时, Python的垃圾回收机制就会将对象回收 a = "larry" b = a larry这个字符串对象, 在第一行被贴了a标签后, 引用计数为1, 之后在第二行, 由贴上了b标签, 此时, 该字符串对象的引用计数为 a = "larry" b = a del a 注意: 在Python语言中, del语句操作某个对象的时候, 并不是直接将该对象在内存中删除, 而是将该对象的引用计数-1 &g

  • Python小白垃圾回收机制入门

    引用计数 Python默认的垃圾收集机制是"引用计数",每个对象维护了一个ob_ref字段.它的优点是机制简单,当新的引用指向该对象时,引用计数加1,当一个对象的引用被销毁时减1,一旦对象的引用计数为0,该对象立即被回收,所占用的内存将被释放.它的缺点是需要额外的空间维护引用计数,不过最主要的问题是它不能解决"循环引用". 什么是循环引用?A和B相互引用而再没有外部引用A与B中的任何一个,它们的引用计数虽然都为1,但显然应该被回收,例子: a = { } # a 的

  • Python的垃圾回收机制深入分析

    一.概述: Python的GC模块主要运用了"引用计数"(reference counting)来跟踪和回收垃圾.在引用计数的基础上,还可以通过"标记-清除"(mark and sweep)解决容器对象可能产生的循环引用的问题.通过"分代回收"(generation collection)以空间换取时间来进一步提高垃圾回收的效率. 二.引用计数 在Python中,大多数对象的生命周期都是通过对象的引用计数来管理的.从广义上来讲,引用计数也是一种垃

随机推荐