Python的垃圾回收机制

Stella981
• 阅读 347

垃圾回收机制

「垃圾回收(GC)」 大家应该多多少少都了解过,什么是垃圾回收呢?垃圾回收GC的全拼是 Garbage Collection,在维基百科的定义是:在计算机科学中,垃圾回收(英语:Garbage Collection,缩写为GC)是一种自动的内存管理机制。当一个电脑上的动态内存不再需要时,就应该予以释放,以让出内存,这种内存资源管理,称为垃圾回收。我们都知道在C/C++里用户需要自己管理维护内存,自己管理内存是很自由,可以随意申请、释放内存,但是极易会出现内存泄露,悬空指针等问题;像现在的高级语言Java,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的,就是垃圾对象了,我们就需要对它们进行垃圾回收。例如:

Python的垃圾回收机制

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

那么根集合就是a和b了,然后我们从a和b出发开始标记并恢复引用计数: Python的垃圾回收机制

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

分代回收

分代回收是一种以空间换时间的操作方式,Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了3代,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代)。它们对应3个链表,它们的垃圾收集频率随对象的存活时间的增大而减小。
新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,即当垃圾回收器中新增对象减去删除对象达到相应的阈值时,就会对这一代对象启动垃圾回收,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。同时,分代回收是建立在标记清除技术基础之上。事实上,分代回收基于的思想是,新生的对象更有可能被垃圾回收,而存活更久的对象也有更高的概率继续存活。因此,通过这种做法,可以节约不少计算量,从而提高Python的性能。

总结

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

最后,感谢女朋友在工作和生活中的包容、理解与支持 !

Python的垃圾回收机制

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Symbol卢 Symbol卢
2年前
js垃圾回收机制原理给你聊的明明白白
前言大多数语言都是提供自动内存管理机制,比如C、Java,JavaScript。自动内存管理机制也就是我们经常听到的垃圾回收机制。好神奇哦,语言会收垃圾,哈哈😄,不过这里的垃圾,可不是家里面的厨余垃圾啥的,而是一些不再使用的变量所占用的内存。我们的js的执行环境会自动对这些垃圾进行回收,也就是释放那些不再使用的变量所占用的内存,收垃圾的过程会按照固定的
Wesley13 Wesley13
2年前
java中的GC和内存泄漏
java中的GC1.GC是什么?为什么要有GC? GC是垃圾回收的意思。是指JVM清理不再使用的对象释放内存。垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存.2\.需要GC的内存区域垃圾回收区域:主要针对无用堆对象回
3A网络 3A网络
1年前
Java 垃圾回收机制
面试必问:Java垃圾回收机制介绍在C/C中,程序员负责对象的创建和销毁。通常程序员会忽略无用对象的销毁。由于这种疏忽,在某些时候,为了创建新对象,可能没有足够的内存可用,整个程序将异常终止,导致OutOfMemoryErrors。但是在Java中,程序员不需要关心所有不再使用的对象。垃圾回收机制自动销毁这些对象。垃圾回收机制是守护线
Wesley13 Wesley13
2年前
Java系列笔记
Java垃圾回收概况  JavaGC(GarbageCollection,垃圾收集,垃圾回收)机制,是Java与C/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢。这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制。概括地说,该机制对JVM(J
Wesley13 Wesley13
2年前
Java 内存区域和GC机制
Java垃圾回收概况  JavaGC(GarbageCollection,垃圾收集,垃圾回收)机制,是Java与C/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢。这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制。概括地说,该机制对JVM
Wesley13 Wesley13
2年前
.NET中的GC垃圾回收
本章将和大家分享.NET中的GC垃圾回收。托管堆垃圾回收CLR提供GC。1、什么样的对象需要垃圾回收?  托管资源引用类型  托管资源和非托管资源:    托管的就是CLR控制的,例如:new的对象、string字符串、变量等;    非托管不是CLR能控制的,例如:数据库连接、文件流、句柄、打印机连接等;    u
Stella981 Stella981
2年前
JavaScript性能优化
❝性能优化是一个很大的概念,性能优化的方向有很多比如底层、框架层面上、页面上等等,本篇文章介绍的是JavaScript语言的优化,了解JavaScript的运行的机制❞本片文章主要从如下几个方面讲解:内存管理垃圾回收与常见GC算法V8引擎的垃圾回收Perf
Stella981 Stella981
2年前
Python垃圾回收机制
对于Python垃圾回收机制主要有三个,首先是使用引用计数来跟踪和回收垃圾,为了解决循环引用问题,就采用标记清除的方法,标记清除的方法所带来的额外操作实际上与系统中总的内存块的总数是相关的,当需要回收的内存块越多,垃圾检查带来的额外操作就越多,为了提高垃圾收集的效率,采用“空间换时间的策略”,即使用分代机制,对于长时间没有被回收的内存就
Stella981 Stella981
2年前
JVM的GC算法总结
Java程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经失去标记,程序用不了它们了,对程序而言它们已经废弃),为了确保程序运行时的性能,java虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC),这就是我们的垃圾回收机制,关于垃圾回收我总结了一下几种:标记–清除算法(MarkSweep)