python的GC

在python中,垃圾回收机制主要是以引用计数为主,以标记清除和分代回收机制为辅。

在python中一切皆对象,变量的本质其实就是对象的一个指针;比如:a=1 其实先分配内存(创建对象)存储数据“1”,然后a变量中指向对象(变量a存储着对象的内存地址)。

  • 不可变对象:变量相对于对象的指向不会发生改变(str, tuple等)
    • 具有相同值的不可变对象,变量指向的是同一个对象
    • 只要不可变对象的值发生改变,变量就会指向重新创建的对象
  • 可变对象:变量相对于对象的指向会发生改变(list等)
    • 具有相同值的可变对象,变量指向的是不同的对象
    • 允许可变对象存储数据的值发生改变,而不影响变量的指向

因为python运行过程中会使用各种变量,所以如果对象的占用内存管理不当,程序会由于内存溢出而异常终止。

引用计数

在python中万物皆对象,每个对象的核心就是一个结构体PyObject。每个对象维护一个ob_refcnt字段作为引用计数器,用来记录该对象当前被引用的次数;程序在运行的过程中会实时更新ob_refcnt的值,每当新的引用指向该对象时,它的引用计数ob_refcnt加1,每当该对象的引用失效时计数ob_refcnt减1;一旦该对象的引用计数值为0,该对象立即被回收,即它所占用的内存空间会被立即释放。

引用计数加1:

  • 对象被创建,如a=999
  • 对象被引用,如b=a
  • 对象被作为参数传入到函数,如func(a)
  • 对象作为元素存储在容器中,如lst=[a]

引用计数减1:

  • 对象别名被显式销毁 ,如del a
  • 对象别名被赋予新的对象,如a=888
  • 一个对象离开它的作用域,如func函数执行完毕后func函数中的局部变量
  • 对象所在的容器被销毁或从容器中删除对象

优点:

  • 简单高效
  • 实时:一旦一个对象的引用计数归零,内存立即释放

缺点:

  • 资源消耗:维护引用计数的次数和引用赋值成正比
  • 无法解决循环引用问题

标记清除

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

  • 标记:GC将所有的活动对象打上标记
  • 清除:GC把没有被打上标记的非活动对象进行回收
  • 对象之间通过引用(每个容器对象维护两个额外的指针,分别指向前后两个容器对象, 用来将容器对象组成一个链表)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被回收的非活动对象。根对象就是全局变量、调用栈、寄存器。
    • python解释器(Cpython)维护了两个这样的双端链表:一个链表存放着需要被扫描的容器对象(Object to Scan/root链表),另一个链表存放着临时不可达对象(Unreachable)。
    • gc启动的时候,会逐个遍历”Object to Scan”链表中的容器对象,并且将当前对象所引用的所有对象的gc_ref(记录当前引用计数的变量ref_count的副本,初始值为ref_count的大小)减一,这一步操作就相当于解除了循环引用对引用计数的影响。
    • 接着,gc会再次扫描所有的容器对象,如果对象的gc_ref值为0,那么这个对象就被标记为GC_TENTATIVELY_UNREACHABLE,并被移至”Unreachable”链表中。如果对象的gc_ref不为0,那么这个对象就会被标记为GC_REACHABLE。同时当gc发现有一个节点是可达的,那么他会递归地将从该节点出发可以到达的所有节点标记为GC_REACHABLE,如果可达节点当前在”Unreachable”链表中的话,还需要将其移回到”Object to Scan”链表中

    • 第二次遍历的所有对象都遍历完成之后,存在于”Unreachable”链表中的对象就是真正需要被释放的对象,gc随即释放之。

上面描述的垃圾回收的阶段,会暂停整个应用程序,等待标记清除结束后才会恢复应用程序的运行。

缺点

  • 清除非活动的对象前必须顺序扫描整个堆内存。

分代回收

垃圾回收时,python不能进行其它的任务,频繁的垃圾回收将大大降低Python的工作效率,因此python 通过分代回收“以空间换时间”的方法提高垃圾回收效率。gc 模块可设置自动垃圾回收的阀值(创建数量与释放数量的差值,如果存在循环引用,创建数量>释放数量),即通过gc.get_threshold函数获取到的长度为 3 的元组,例如(700,10,10)代表gc每一代垃圾回收所触发的阈值;每一次计数器的增加,gc 模块就会检查增加后的计数是否达到阀值的数目,如果是,就会执行对应的代数的垃圾检查,然后重置计数器。

  • python 将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,python 将内存分为了 3代,分别为年轻代(第 0 代)、中年代(第 1 代)、老年代(第 2 代),他们对应的是 3 个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。
  • 新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,python 垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。

分代回收是建立在标记清除技术基础之上,同样作为辅助垃圾收集技术处理那些容器对象。(分代回收使用不同的链表追踪活跃对象,每创建一个对象都会被加入年轻代链表;被分配计数值与被释放计数值的差值达到阙值时进行标记回收,一代链表中遵循同样的方法,将活跃对象移动到二代链表中)

Notes

可通过sys.getrefcount()来查询对象的引用计数。注意当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用。因此getrefcount()所得到的结果,总会比期望的多1。

可通过gc.collect()手动启动垃圾回收。

pandas和GC:

#deleting references
del df
#triggering collection
gc.collect()
#finally check memory usage
memory_usage()

python的内存管理机制:Python的内存管理内存总共分为4层,Layer2为内存池,Layer3为对象缓冲池;Layer3是建立在Layer2基础上的:

  • 内存池:block、pool、arean;对 python 中的一些常用对象,比如整数对象、字符串对象等,Python 又构建了更高抽象层次的内存管理策略;GC 所在层次
  • 对象缓冲池:对象

猜你喜欢

转载自blog.csdn.net/qq_34276652/article/details/112726751
GC