python入门笔记——垃圾回收(引用计数、标记清除和分代回收)
垃圾回收(GC)负责的主要任务
1.为新生成的对象分配内存
2.识别那些垃圾对象
3.从垃圾对象那回收内存
python是默认开启垃圾回收的,一般有三种情况会触发垃圾回收:
1.当gc模块的计数器达到阈值的时候,会自动回收垃圾
2.手动调用gc模块里的gc.collect()(记得import gc),会手动回收垃圾
3.程序退出的时候,python解释器回收垃圾
python采用的是引用计数机制为主
标记清除和分代收集两种机制为辅的机制
引用计数
引用计数是这样一种机制:
在python内部,每个对象有新的引用时,都会有一个自己单独的引用计数值
当引用它的对象被删除,这个值就会减少,最后当计数值变成0后,该对象的生命就结束了
我们可以总结一般导致计数+1和-1的情况
导致引用计数+1的情况:
1.对象被创建
2.对象被引用
3.对象作为参数被传入到一个函数中
4.对象作为元素被存储在容器中
导致引用计数-1的情况:
1.对象的别名被显式销毁
2.对象的别名被赋给了新的对象
3.一个对象离开了它的作用域(如一个函数执行完后,它里面的局部变量)
4.对象所存在的容器被销毁,或从容器中删除了对象
import sys
# sys模块中的sys.getrefcount函数能查看对象引用次数
a = []# 1次
print(sys.getrefcount(a))# 这里也引用了一次a,但是这里在引用后会给次数-1。同时打印对象引用次数
b = a# 1次
c = b# 1次
d = b# 1次
print(sys.getrefcount(a))# 这里打印出来的包括本次应是5次
输出结果:
2
5
我们可以看到,引用计数具有简单和实时性的优点
实时性在于:一旦没有引用,内存就直接释放了,不用等到什么特定时机
且回收内存的时间也分摊到了各个时间
但引用计数也有缺点,一在于维护引用计数会消耗资源
二在于一个致命的问题:可能存在循环引用,如两对象相互引用,哪怕不存在其他对象对它们的引用
它们的引用计数仍然为1,所占用的内存永远无法被回收
故需要引入新的回收机制来完善(标记清除和分代收集)
标记清除
标记清除是这样一种机制:
在触发某种情况后会触发一个扫描机制,扫描该链表中的每一个元素,并依次去检查每个元素的子元素、子子元素等等,相当于是将对象以引用指针连在一起形成一个有向图,扫描机制以有向图的有向边去遍历对象,当发现遍历到的某个对象是其本身时,即证明存在循环引用
接着将它们双方的引用计数器-1,如果此时引用计数器-1后的结果是0了,那么对其进行垃圾回收
当然,必须是将循环引用的双方都删除时,才能进行垃圾回收
分代回收
python的内部c代码将对象分为三代:0代、1代、2代
其中,新创建的对象会在0代
当0代中的对象数量达到某个设定的阈值后,就会对0代触发一次扫描机制,将其中的可以被回收的对象回收掉,循环引用的对象计数自减1(如果到0了将被垃圾回收掉),然后,将不会被回收的对象放到1代去
同样,当1代对象达到一定阈值(1代的值是记录0代扫描的次数),也会触发扫描机制,后将未被回收的对象升到2代
2代也有类似的扫描机制处理
通过此种方法,你的代码持续访问的活跃对象,会从0代逐渐转移到1代再到2代,通过设定的阈值,python可以在不同的时间间隔处理这些不同代的对象,而导致其中0代处理将最为频繁,然后是1代,最后是2代
python内存优化
小整数池和大整数对象池
python为了优化速度,使用了小整数对象池,避免为整数频繁申请和销毁内存空间
python对小整数的定义是[-5,256],这些整数对象是提前建立好的,不会被垃圾回收,我们只要拿来用就行了
大整数池是没有提前创建好的,需要自己创建
但创建好后,会把创建的整数对象保存在池子里,后面也不需要创建了,直接可以拿来使用
这里可以通过查看地址来更加了解
a = 1
b = 1
print(id(a))
print(id(b))
# 这里可以看到,a和b的地址是一样的,这说明它们只是使用了python提前创建好的1
# 只是指向了python初始化创建1的内存地址
# 当然这里你使用大整数也是类似的结果,因为第一次创建的时候就保存了大整数对象在池子里
输出结果:
140709064672944
140709064672944