python中级之垃圾回收机制

发布时间 2023-12-04 16:17:40作者: Xiao0101

引入

image

当解释器在执行到变量赋值的时候,例如:name = 'xiaoying',那么会向内存空间申请一块空间来存放变量的值,而内存的空间是有限的,并不能无限制的存放,那么这就涉及到了一个回收的问题,当一个变量值没用了之后,简称(垃圾),那么垃圾就该放进垃圾桶中进行回收,将其所占用的内存空间释放回收掉。那么什么样的变量是没有用的呢?

从逻辑方面分析,定义的变量是为了后续将其引用,要通过内存的门牌号找到该变量对其进行直接引用或者间接引用,所以当一个变量值没有被任何的'人'引用时,就称之他为垃圾,而垃圾自然是不被需要的,所以就需要将他回收掉
'''
一、直接引用
    x = 10
    print(x)

二、间接引用
    l = ['a','b',x]
    print(l[2])
'''
而CPython就为我们提供了这样 的回收机制来帮助我们将'垃圾回收'

什么是垃圾回收机制?

垃圾回收机制(简称GC)是Python解释器自带一种机制,专门用来回收不可用的变量值所占用的内存空间

为什么要用垃圾回收机制?

程序运行过程中会申请大量的内存空间,而对于一些无用的内存空间如果不及时清理的话会导致内存使用殆尽(内存溢出),导致程序崩溃,因此管理内存是一件重要且繁杂的事情,而python解释器自带的垃圾回收机制把程序员从繁杂的内存管理中解放出来。

垃圾回收机制储备知识

堆区和栈区

  • 在定义变量时,变量名与变量值都是需要存储的
  • 分别对应内存中的两块区域:
    • 堆区
      • 变量名与值内存地址的关联关系存放于栈区
    • 栈区
      • 变量值存放于堆区,内存管理回收的则是堆区的内容,
  • 示例
    • 定义了两个变量x = 10、y = 20,详解如下图

image

  • 当我们执行x=y时,内存中的栈区与堆区变化如下

image

直接引用和简介引用

  • 直接引用指的是从栈区出发直接引用到的内存地址。
  • 间接引用指的是从栈区出发引用到堆区后,再通过进一步引用才能到达的内存地址。
l2 = [20, 30] # 列表本身被变量名l2直接引用,包含的元素被列表间接引用 
x = 10  	  # 值10被变量名x直接引用
l1 = [x, l2]  # 列表本身被变量名l1直接引用,包含的元素被列表间接引用
  • 图解

image

垃圾回收机制原理分析

Python的GC模块主要运用了“引用计数”(reference counting)来跟踪和回收垃圾。在引用计数的基础上,还可以通过“标记-清除”(mark and sweep)解决容器对象可能产生的循环引用的问题,并且通过“分代回收”(generation collection)以空间换取时间的方式来进一步提高垃圾回收的效率。

引用计数

image

'''
引用计数:顾名思义就是变量值被变量名关联的次数,例如:
        name = 'xiaoying'
        这里xiaoying被变量名name所关联,引用计数就 +1

引用计数的增加:之前name = 'xiaoying'  # 此刻引用计数为1
             那么将name在赋予给一个变量名,例如:
             x = name  # 将name的内存地址也给到了x,此时x与name都关联到了'xiaoying'的内存地址,此刻引用计数为2

引用计数的减少:可以通过解绑的方式,这里通过del来完成,例如
             del x  # del的意思是解除变量名x与变量值之间的关联关系,此时引用计数就 -1
             或者
             name = 'cc'  # name先与值'xiaoying'解除关联,再与'cc'建立了关联,变量值'xiaoying'引用计数 -1,此刻'xiaoying'的内存地址引用计数为0,那么久将触发GC机制回收

'''

image

引用计数扩展阅读--循环引用

变量值的关联+1或者减少,都会触发引用计数的机制,这就存在了效率问题。如果说单单只存在执行效率的话,那么还是幸运的,但引用计数还存在着一个致命的问题,即是循环引用。
'''
# 定义两个列表
l1 = [111, ]  # 列表[111,],引用计数+1
l2 = [222, ]  # 列表[222,],引用计数+1

l1.append(l2)  # l1=[值111的内存地址,l2列表的内存地址]  
l2.append(l1)  # l2=[值222的内存地址,l1列表的内存地址]
# 两个列表之间存在相互引用,在当将两个之间的关联解除是,但是值的引用并不会因为解除而变为0,那么就不会触发GC机制将占用的内存空间回收,例如:
del l1  # 列表1的引用计数减1,列表1的引用计数变为1
del l2  # 列表2的引用计数减1,列表2的引用计数变为1
'''
此刻,两个列表之间的引用计数都为1,是两个列表互相引用,但两个列表都没有被任何对象关联,所以无法再引用到他们,所以他们两个所占用的内存空间应该被回收,但由于互相引用在,之前的引用计数则不会有作用,无疑这样的问题是致命的,所以Python引入了"标记清除"和"分代回收"来解决效率与循环引用的问题

标记-清除

容器的对象,例如:列表,字典,集合,类等等可以包含对其他对象的引用,都可能产生循环引用的问题,标记清除就是解决循环引用问题的机制
关于变量的存储,在内存中有两块区域:堆区和栈区,其中变量名在内存栈区,变量名存的是内存地址,变量值在内存堆区,GC机制回收的是堆区的内容

image

当我们执行x = y时,内存空间中则会发生这样的变化

image

将原来x 与 10的绑定关系解除,x 与 20绑定在一块
标记清除算法的做法是当应用程序可用的内存空间被耗尽的时候,就会停止整个程序,然后进行两项工作,第一项是标记,第二项则是清除

'''
# 1、标记:
    标记的过程就是遍历所有的GC Roots对象(栈区中所有的内容或者线程都可以作为GC Roots对象),然后将所有GC Roots对象可以直接访问到的或者间接访问到的标记为存活,其余则应该被清除
    通俗来说,就是标记的整个过程相当于从栈区出发的一条连接线,连到堆区后,再由堆区连到对应的内存地址,凡是可以被这条从栈区出发的线找到的,则都标记为存活
'''
例如之前的两个列表可以做图得到对应的关系:

image

当我们同时del l1   del l2之后

image

这样的话在标记清除的机制下,发现栈区中不在有一条'线'可以连接到堆区的值,只剩下堆区的l1与l2相互引用,于是这两个列表都不会被标记为存活,两者都会被清理掉,这样就解决了循环引用带来的问题

分代回收

背景

基于引用计数的回收机制,每次回收内存,都需要把所有对象的引用计数都遍历一遍,这是非常消耗时间的,于是引入了分代回收来提高回收效率,分代回收采用的是用“空间换时间”的策略。

分代

分代回收的核心思想是:在历经多次扫描的情况下,都没有被回收的变量,gc机制就会认为,该变量是常用变量,gc对其扫描的频率会降低,具体实现原理如下:
分代指的是根据存活时间来为变量划分不同等级(也就是不同的代)

新定义的变量,放到新生代这个等级中,假设每隔1分钟扫描新生代一次,如果发现变量依然被引用,那么该对象的权重(权重本质就是个整数)加一,当变量的权重大于某个设定得值(假设为3),会将它移动到更高一级的青春代,青春代的gc扫描的频率低于新生代(扫描时间间隔更长),假设5分钟扫描青春代一次,这样每次gc需要扫描的变量的总个数就变少了,节省了扫描的总时间,接下来,青春代中的对象,也会以同样的方式被移动到老年代中。也就是等级(代)越高,被垃圾回收机制扫描的频率越低

回收:

回收依然是使用引用计数作为回收的依据

image

虽然分代回收可以起到提升效率的效果,但也存在一定的缺点:

#例如一个变量刚刚从新生代移入青春代,该变量的绑定关系就解除了,该变量应该被回收,但青春代的扫描频率低于新生代,所以该变量的回收就会被延迟。