Python的垃圾回收机制

Python的垃圾回收机制

什么是垃圾回收

  • 垃圾回收机制(简称GC)是Python解释器自带的一种机制

  • 专门用来回收不可用的变量值所占用的内存空间(在内存中,没有变量名指向的数据都是垃圾数据)

    # a是正常变量赋值 20是未被赋值的垃圾数据
    a = 10
    20
    

为什么要有垃圾回收机制

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

引用计数

垃圾回收机制的原理

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

而引用计数就是:变量值被变量名关联的次数

age = 18	# 这个18便被age关联 因此他的引用计数为1
m = age		# 把age的内存地址再次赋值给m,此时m和age都关联了18,所以现在18的引用计数为2
del m		# del会解除变量名与变量值的关联 因此现在18的引用计数又减少为1

引用计数的弱点

  • 引用计数机制存在着一个致命的弱点,即循环引用(也称交叉引用)
l1 = ['xxx'] 
l2 = ['yyy']
l1.append(l2) 
l2.append(l1)
# 此时打印l1会陷入死循环
# Python的打印函数(如print())在遇到循环引用时会检测到并处理这种情况,以避免陷入无限循环。代替无限打印嵌套结构,它使用[...]作为一个占位符,提示存在循环引用。
# 因此结果将输出为:['xxx', ['yyy', [...]]]
  • 了解了什么是循环引用后便要解释为什么循环引用是引用计数的致命弱点
# 如下我们定义了两个列表,简称列表1与列表2,变量名l1指向列表1,变量名l2指向列表2
l1 = ['xxx']  # 列表1被引用一次,列表1的引用计数变为1
l2 = ['yyy']  # 列表2被引用一次,列表2的引用计数变为1
l1.append(l2)  # 把列表2追加到l1中作为第二个元素,列表2的引用计数变为2
l2.append(l1)  # 把列表1追加到l2中作为第二个元素,列表1的引用计数变为2

# 1. l1与l2之间有相互引用
# 2. l1 = ['xxx'的内存地址,列表2的内存地址]
# 3. l2 = ['yyy'的内存地址,列表1的内存地址]

print(l1)  # ['xxx', ['yyy', [...]]]

print(l2)  # ['yyy', ['xxx', [...]]]

del l1  # 列表1的引用计数减1,列表1的引用计数变为1
del l2  # 列表2的引用计数减1,列表2的引用计数变为1

print(l1)  # name 'l1' is not defined

print(l2)  # name 'l2' is not defined
  • 此时只剩下表1与表2之间相互引用

  • 但此时两个列表的引用计数均不为0,但两个列表不再被任何其他对象关联,没有任何人可以再引用到它们

    • 所以它俩占用内存空间应该被回收,但由于相互引用的存在,每一个对象的引用计数都不为0
    • 因此这些对象所占用的内存永远不会被释放,所以循环引用是致命的,这与手动进行内存管理所产生的内存泄露毫无区别。
  • 所以Python引入了“标记-清除” 与“分代回收”来分别解决引用计数的循环引用与效率低的问题

标记-清除

该算法分为两个阶段:

  • 标记
    • 从一组根对象开始,遍历整个对象图,标记所有可达的对象。根对象可以是全局变量、活动的函数和方法栈、CPU寄存器等。只有被标记的对象被视为活动对象,其它未被标记的对象将被认为是垃圾对象
  • 清除
    • 在标记阶段之后,清除所有未被标记的对象,并回收它们占用的内存空间。清除后,内存空间将被重新分配给新的对象

用通俗的话来讲就是当程序开始运行时垃圾回收器会从根对象起遍历所有对象图,可达的对象会被标记,而没有被标记的不可达对象就会直接进行清除

标记-清除算法能够解决循环引用带来的内存泄漏问题,但也会带来一定的性能开销。为了降低性能开销,Python还使用了分代回收(Generational Collection)策略

分代回收

分代回收的核心思想
  • 分代回收是一种垃圾回收的策略,其核心思想是根据对象的存活时间将其划分为不同的代(generation)。
  • 在Python中,通常将内存分为三代:
    • 新生代(young generation): 包含了大部分新创建的对象,其存活时间较短。
    • 青春代(youth generation): 包含了已经经历过垃圾回收一次的新生代对象,存活时间较长。
    • 老年代(old generation): 包含了已经经历过多次垃圾回收的对象,存活时间较长。
具体实现原理
  • 大部分对象在其刚被创建的时候往往很快变得不可达,即它们的生命周期较短。
  • 部分对象在经历了一次垃圾回收后,仍然存活的概率较高,即它们的生命周期较长。
  • 因此,分代回收将内存划分为不同的代,并根据代的特点采用不同的垃圾回收策略。
    • 新生代中的对象:采用较为频繁的垃圾回收,通常使用标记-复制算法,即标记存活对象,然后将存活对象复制到另一块内存中,最后清理掉原有的内存。这能有效处理新生代中大部分对象的垃圾回收
    • 青春代中的对象:采用标记-清除算法,其目标是处理在新生代中经历了一次回收的对象。由于这些对象的生命周期较长,不需要频繁地进行回收
    • 老年代中的对象:采用标记-清除算法标记-整理算法。老年代的对象生命周期更长,采用相对稳定的垃圾回收策略
分代回收的问题
  • 新生代到青春代的延迟问题: 当一个变量从新生代移入青春代时,其绑定关系解除,应该被回收。然而,由于青春代的扫描频率低于新生代,可能导致这些变量的垃圾没有得到及时的清理。
  • 权重提升的不精确性: 分代回收中通过给对象的权重进行提升来判断其生命周期。然而,这种机制并不完美,可能存在一些不够准确的情况,导致有些垃圾对象没有被及时清理。
  • 无法解决循环引用的所有问题: 尽管分代回收可以处理一些循环引用的情况,但并不能解决所有可能的循环引用问题。在某些情况下,仍然需要手动处理循环引用,例如通过显式地断开引用或使用弱引用。
  • 全局解释器锁(GIL): 在多线程的情况下,全局解释器锁可能会影响垃圾回收的效率,尤其是对于CPU密集型的任务。在处理大规模数据或高并发情况下,需要考虑GIL的影响。
  • 没有十全十美的方案:
    • 毫无疑问,如果没有分代回收,即引用计数机制一直不停地对所有变量进行全体扫描,可以更及时地清理掉垃圾占用的内存,但这种一直不停地对所有变量进行全体扫描的方式效率极低,所以我们只能将二者中和。
  • 21
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值