Python 垃圾回收机制详解

垃圾回收机制:GC机制

在计算机科学中,垃圾回收(英语:Garbage Collection,缩写为GC)是指一种自动的存储器管理机制。当某个程序占用的一部分内存空间不再被这个程序访问时,这个程序会借助垃圾回收算法向操作系统归还这部分内存空间。垃圾回收器可以减轻程序员的负担,也减少程序中的错误。垃圾回收最早起源于LISP语言。目前许多语言如 Python、Java、C# 都支持垃圾回收器。

垃圾回收机制:GC机制
	Python:
	1.引用计数机制为主
	    如何获取一个对象的引用计数?
	    sys.getrefcount(p)
	    刚创建对象引用计数为 2
	
	    a.增加引用计数操作
	        1.如果有新的对象使用该对象 +1
	        2.装进列表 +1
	        3.作为函数参数 +1
	        
	    b.减少引用计数操作
	        1.如果有新的对象使用该对象,新对象不再使用时 -1
	        2.从列表中删除 -1
	        3.函数调用结束 -1
	        4.del 显示销毁
	        
	2.隔代回收机制为辅
		原理:
			随着时间的推进,程序冗余对象逐渐增多,
			达到一定数量(阈值),系统进行回收(0代,1代,2代).
			默认初始阈值:(7001010.

引用计数机制

  • 原理:
python 里每一个东西都是对象,它们的核心就是一个结构体:PyObject

 typedef struct_object {
    
    
 	  	int ob_refcnt;
 		struct_typeobject *ob_type;
	} PyObject;
	
PyObject 是每个对象必有的内容,其中 ob_refcnt 就是做为引用计数。
当一个对象有新的引用时,它的 ob_refcnt 就会增加,当引用它的对象被删除,它的ob_refcnt就会减少。

#define Py_INCREF(op)   ((op)->ob_refcnt++) //增加计数
	#define Py_DECREF(op) \ //减少计数
    if (--(op)->ob_refcnt != 0) \
        ; \
   	else \
        __Py_Dealloc((PyObject *)(op))
        
当引用计数为 0 时,该对象生命就结束了。
  • 优点:
    1.简单
    2.实时性:一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。
    实时性还带来一个好处:处理回收内存的时间分摊到了平时。

  • 缺点:
    1.维护引用计数消耗资源
    2.循环引用的问题无法解决(DOS窗口,查看内存tasklist,或者内存表,任务管理器)

  • 示例代码:

import sys


class Person:
    # 创建对象开辟内存时调用
    def __new__(cls, *args, **kwargs):
        print('开辟内存空间')
        return super(Person, cls).__new__(cls)

    # 初始化方法
    def __init__(self, name):
        self.name = name
        print(f'创建对象:{id(self)}')

    # 对象被系统回收前,调用该方法
    def __del__(self):
        print(f'回收对象:{id(self)}')

    def who(self):
        print('----------')
        print(f'我叫{self.name}')
        print('----------')
        print(f'作为函数参数以及被函数对象使用,p的引用计数为:{sys.getrefcount(p)}')
        print('-------------减少引用计数--------------')


if __name__ == '__main__':
    p = Person('李四')
    print(f'p的引用计数为:{sys.getrefcount(p)}')

    print('-------------增加引用计数--------------')

    # 如果有新的对象使用该对象 +1
    p1 = p
    print(f'有新的对象使用该对象,p的引用计数为:{sys.getrefcount(p)}')

    # 装进列表 +1
    list_ = [p]
    print(f'装进列表,p的引用计数为:{sys.getrefcount(p)}')

    # 作为函数参数以及被函数对象使用 +2
    p.who()
    # 函数调用结束以及函数对象不再使用该对象时 -2
    print(f'函数调用结束以及函数对象不再使用该对象时,p的引用计数为:{sys.getrefcount(p)}')

    # 如果有新的对象使用该对象,新对象不再使用时 -1
    p1 = 100
    print(f'有新的对象使用该对象,新对象不再使用时,p的引用计数为:{sys.getrefcount(p)}')

    # 从列表中删除 -1
    list_.remove(p)
    print(f'从列表中删除,p的引用计数为:{sys.getrefcount(p)}')

    # del 显示销毁
    del p

    print('------------程序结束--------------')

运行结果:

开辟内存空间
创建对象:1503289216920
p的引用计数为:2
-------------增加引用计数--------------
有新的对象使用该对象,p的引用计数为:3
装进列表,p的引用计数为:4
----------
我叫李四
----------
作为函数参数以及被函数对象使用,p的引用计数为:6
-------------减少引用计数--------------
函数调用结束以及函数对象不再使用该对象时,p的引用计数为:4
有新的对象使用该对象,新对象不再使用时,p的引用计数为:3
从列表中删除,p的引用计数为:2
回收对象:1503289216920
------------程序结束--------------

这里主要说下函数调用的过程:

在实例对象 p 调用实例方法:who() 时,相当于给 who() 方法传了参数:self = p,这里就等同于 p1 = p 操作,引用计数 +1.

实例对象 p 调用 who() 方法,p 对象又指向 who() 方法的在内存中的地址,此时引用计数 +1,所以不管是一个对象调用实例方法还是普通函数,如果需要将该对象作为参数传入的话,引用计数 +2,函数调用结束后引用计数 -2,否则只 +1 和 -1.

当把 del p 这句代码注释掉时,结果为:

开辟内存空间
创建对象:2786294632344
p的引用计数为:2
-------------增加引用计数--------------
有新的对象使用该对象,p的引用计数为:3
装进列表,p的引用计数为:4
----------
我叫李四
----------
作为函数参数以及被函数对象使用,p的引用计数为:6
-------------减少引用计数--------------
函数调用结束以及函数对象不再使用该对象时,p的引用计数为:4
有新的对象使用该对象,新对象不再使用时,p的引用计数为:3
从列表中删除,p的引用计数为:2
------------程序结束--------------
回收对象:2786294632344

程序结束之后 p 对象才被系统自动回收,原因是:

在调用 del p 手动回收之前,p 的引用计数已经是 2 了,当再次显示调用 del 删除对象 p 时,引用计数 -1 后值为 1,系统会每隔一段时间扫描每个对象的引用计数,扫描后对每个对象的引用计数进行 -1 操作,如果 -1 后引用计数为 0,则将该对象进行回收,这也是为什么在创建一个新的对象之后,引用计数等于 2 的原因。

隔代回收机制

  • 原理:
分代回收是用来解决交叉引用(循环引用),并增加数据回收的效率. 

原理:通过对象存在的时间不同,采用不同的算法来回收垃圾. 

形象的比喻, 三个链表,零代链表上的对象(新创建的对象都加入到零代链表),
引用数都是一,每增加一个指针,引用加一,
随后 python 会检测列表中的互相引用的对象,根据规则减掉其引用计数. 

GC 算法对链表一的引用减一,引用为 0 的清除,不为 0 的到链表二,链表二也执行
GC 算法,链表三一样. 

存在时间越长的数据,越是有用的数据.
  • 隔代回收触发时间:(GC 阈值)
随着你的程序运行,Python 解释器保持对新创建的对象,
以及因为引用计数为零而被释放掉的对象的追踪。从理论上说,
这两个值应该保持一致,因为程序新建的每个对象都应该最终被释放掉。
当然,事实并非如此。因为循环引用的原因,从而被分配对象的计数值
与被释放对象的计数值之间的差异在逐渐增长。一旦这个差异累计超过某个阈值,
则 Python 的收集机制就启动了,并且触发上边所说到的零代算法,
释放“浮动的垃圾”,并且将剩下的对象移动到一代列表。
随着时间的推移,程序所使用的对象逐渐从零代列表移动到一代列表。
而 Python 对于一代列表中对象的处理遵循同样的方法,一旦被分配计数值
与被释放计数值累计到达一定阈值,Python 会将剩下的活跃对象移动到二代列表。
通过这种方法,你的代码所长期使用的对象,那些你的代码持续访问的活跃对象,
会从零代链表转移到一代再转移到二代。通过不同的阈值设置,Python 可以在
不同的时间间隔处理这些对象。Python 处理零代最为频繁,其次是一代然后才是二代。
  • 示例代码:
import gc
import time


class Person:
    # 创建对象开辟内存时调用
    def __new__(cls, *args, **kwargs):
        print('开辟内存空间')
        return super(Person, cls).__new__(cls)

    # 初始化方法
    def __init__(self):
        print(f'创建对象:{id(self)}')

    # 对象被系统回收前,调用该方法
    def __del__(self):
        print(f'回收对象:{id(self)}')


def start():
    while True:
        p = Person()
        p2 = Person()
        p.v = p2
        p2.v = p
        del p
        del p2
        print(gc.get_threshold())
        print(gc.get_count())
        time.sleep(0.1)


if __name__ == '__main__':
	gc.set_threshold(100, 10, 10)
    start()

这里定义了一个 start() 函数,让对象 p 和 p2 循环引用对方,再分别删除 p 和 p2 两个对象,其中调用 gc 模块中的函数功能如下:为了方便查看运行结果,我们暂时将三个链表的初始值由原来的 (700,10,10) 通过 set_threshold() 函数设定为(100,10,10)

常用函数:
1、gc.get_count()
获取当前自动执行垃圾回收的计数器,返回一个长度为3的列表
2、gc.get_threshold()
获取gc模块中自动执行垃圾回收的频率
3、gc.set_threshold(threshold0[,threshold1,threshold2])
设置自动执行垃圾回收的频率
4、gc.disable()
python3默认开启gc机制,可以使用该方法手动关闭gc机制
5、gc.collect()
手动调用垃圾回收机制回收垃圾

查看上述代码的运行结果:

开辟内存空间
创建对象:2294900461464
开辟内存空间
创建对象:2294902347088
(100, 10, 10)
(2, 6, 1)
开辟内存空间
创建对象:2294902347144
开辟内存空间
创建对象:2294902347200
(100, 10, 10)
(4, 6, 1)
开辟内存空间
创建对象:2294902347256
开辟内存空间
创建对象:2294902347312
(100, 10, 10)
(6, 6, 1)
开辟内存空间
创建对象:2294902347368
开辟内存空间
创建对象:2294902347424
(100, 10, 10)
(8, 6, 1)
开辟内存空间
创建对象:2294902347480
开辟内存空间
创建对象:2294902347536
(100, 10, 10)
(10, 6, 1)
开辟内存空间
创建对象:2294902347592
开辟内存空间
创建对象:2294902347648
(100, 10, 10)
(12, 6, 1)
开辟内存空间
创建对象:2294902347704
开辟内存空间
创建对象:2294902347760
(100, 10, 10)
(14, 6, 1)
开辟内存空间
创建对象:2294902347816
开辟内存空间
创建对象:2294902347872
(100, 10, 10)
(16, 6, 1)
开辟内存空间
创建对象:2294902347928
开辟内存空间
创建对象:2294902347984
(100, 10, 10)
(18, 6, 1)
开辟内存空间
创建对象:2294902348040
开辟内存空间
创建对象:2294902348096
(100, 10, 10)
(22, 6, 1)
开辟内存空间
创建对象:2294902348152
开辟内存空间
创建对象:2294902348208
(100, 10, 10)
(26, 6, 1)
开辟内存空间
创建对象:2294902348264
开辟内存空间
创建对象:2294902348320
(100, 10, 10)
(30, 6, 1)
开辟内存空间
创建对象:2294902348376
开辟内存空间
创建对象:2294902348432
(100, 10, 10)
(34, 6, 1)
开辟内存空间
创建对象:2294902348488
开辟内存空间
创建对象:2294902348544
(100, 10, 10)
(38, 6, 1)
开辟内存空间
创建对象:2294902348600
开辟内存空间
创建对象:2294902348656
(100, 10, 10)
(42, 6, 1)
开辟内存空间
创建对象:2294902348712
开辟内存空间
创建对象:2294902348768
(100, 10, 10)
(46, 6, 1)
开辟内存空间
创建对象:2294902348824
开辟内存空间
创建对象:2294902348880
(100, 10, 10)
(50, 6, 1)
开辟内存空间
创建对象:2294902348936
开辟内存空间
创建对象:2294902348992
(100, 10, 10)
(54, 6, 1)
开辟内存空间
创建对象:2294902349048
开辟内存空间
创建对象:2294902349104
(100, 10, 10)
(58, 6, 1)
开辟内存空间
创建对象:2294902349160
开辟内存空间
创建对象:2294902349216
(100, 10, 10)
(62, 6, 1)
开辟内存空间
创建对象:2294902349272
开辟内存空间
创建对象:2294902349328
(100, 10, 10)
(66, 6, 1)
开辟内存空间
创建对象:2294902349384
开辟内存空间
创建对象:2294902349440
(100, 10, 10)
(70, 6, 1)
开辟内存空间
创建对象:2294902349496
开辟内存空间
创建对象:2294902349552
(100, 10, 10)
(74, 6, 1)
开辟内存空间
创建对象:2294902349608
开辟内存空间
创建对象:2294902349664
(100, 10, 10)
(78, 6, 1)
开辟内存空间
创建对象:2294902349720
开辟内存空间
创建对象:2294902349776
(100, 10, 10)
(82, 6, 1)
开辟内存空间
创建对象:2294902346976
开辟内存空间
创建对象:2294902347032
(100, 10, 10)
(86, 6, 1)
开辟内存空间
创建对象:2294902366392
开辟内存空间
创建对象:2294902366448
(100, 10, 10)
(90, 6, 1)
开辟内存空间
创建对象:2294902366504
开辟内存空间
创建对象:2294902366560
(100, 10, 10)
(94, 6, 1)
开辟内存空间
创建对象:2294902366616
开辟内存空间
创建对象:2294902366672
(100, 10, 10)
(98, 6, 1)
开辟内存空间
创建对象:2294902366728
开辟内存空间
回收对象:2294900461464
回收对象:2294902347088
回收对象:2294902347144
回收对象:2294902347200
回收对象:2294902347256
回收对象:2294902347312
回收对象:2294902347368
回收对象:2294902347424
回收对象:2294902347480
回收对象:2294902347536
回收对象:2294902347592
回收对象:2294902347648
回收对象:2294902347704
回收对象:2294902347760
回收对象:2294902347816
回收对象:2294902347872
回收对象:2294902347928
回收对象:2294902347984
回收对象:2294902348040
回收对象:2294902348096
回收对象:2294902348152
回收对象:2294902348208
回收对象:2294902348264
回收对象:2294902348320
回收对象:2294902348376
回收对象:2294902348432
回收对象:2294902348488
回收对象:2294902348544
回收对象:2294902348600
回收对象:2294902348656
回收对象:2294902348712
回收对象:2294902348768
回收对象:2294902348824
回收对象:2294902348880
回收对象:2294902348936
回收对象:2294902348992
回收对象:2294902349048
回收对象:2294902349104
回收对象:2294902349160
回收对象:2294902349216
回收对象:2294902349272
回收对象:2294902349328
回收对象:2294902349384
回收对象:2294902349440
回收对象:2294902349496
回收对象:2294902349552
回收对象:2294902349608
回收对象:2294902349664
回收对象:2294902349720
回收对象:2294902349776
回收对象:2294902346976
回收对象:2294902347032
回收对象:2294902366392
回收对象:2294902366448
回收对象:2294902366504
回收对象:2294902366560
回收对象:2294902366616
回收对象:2294902366672
创建对象:2294900461464
(100, 10, 10)
(1, 7, 1)

从结果可以看出,相互引用的对象使用 del 是无法删除的,在创建的对象计数达到设定的一代链表的阈值时,系统会采取隔代回收机制,将该链表上的所有对象引用 -1,如果为 0 时,回收对象,若不为 0,则往二代链表放,二代链表的操作同样如此,依次往下一代链表放。

当我们调用 gc 模块的 disable() 函数手动关闭垃圾回收机制时时,结果如下:

开辟内存空间
创建对象:2234394939288
开辟内存空间
创建对象:2234396832936
(100, 10, 10)
(210, 5, 1)
开辟内存空间
创建对象:2234396832992
开辟内存空间
创建对象:2234396833048
(100, 10, 10)
(212, 5, 1)
开辟内存空间
创建对象:2234396833104
开辟内存空间
创建对象:2234396833160
(100, 10, 10)
(214, 5, 1)
开辟内存空间
创建对象:2234396833216
开辟内存空间
创建对象:2234396833272
(100, 10, 10)
(216, 5, 1)
...

可以看出关闭垃圾回收机制后,创建对象达到设定的阈值时并不会主动回收,而会大大地占用内存,消耗系统资源,在程序运行的过程中可去查看任务管理器而得到验证。

猜你喜欢

转载自blog.csdn.net/qq_44214671/article/details/111085012