因为现在的计算机内存比较富裕,所以写程序的时候不会太考虑垃圾回收这方面的知识。但是,出来混总是要还的,既然我们不断的向系统索要资源,那么还是要了解一下什么时候以及怎样把资源还回去
垃圾回收机制
Python中的垃圾回收是以引用计数为主,分代回收为辅
引用计数
在Python中每一个对象的核心都是一个结构体PyObject,它的内部有一个计数器(ob_refcnt),程序在运行的过程中会实时的更新ob_refcnt的值,来反映当前对象的引用计数。当引用计数为0时,那么该对象所占的内存就会被系统释放掉
引用计数+1
- 对象被创建, a = 1
- 对象被引用, b = a
- 对象被当做参数,传入到一个函数中, func(a)
- 对象作为一个元素,存储在容器中, [a, b]
引用计数减一
- 对象别名被显示销毁,del a
- 对象别名被赋予新的对象, a = 2
- 一个对象离开他的作用域
- 对象所在的容器被销毁或者是从容器中删除对象
当指向该对象的内存的引用计数器为0的时候,该内存将会被Python虚拟机销毁
引用计数的优点
- 高效
- 实现逻辑简单
- 具备实时性
- 对象有确定的生命周期
引用计数的缺点
- 虽然实现逻辑简单,但是实现较麻烦。每一个对象需要分配单独的内存来统计引用计数,这无形中加大了内存的负担,并且需要对引用计数进行维护,在维护的时候容易出错。
- 在一些场景下,可能会比较慢。正常来说垃圾会比较平稳运行,但是当需要释放一个大的对象时,需要对引用的所有对象循环嵌套调用,而从可能会花费比较长的时间。
- 循环引用。这是引用计数的致命伤,引用计数无法搞定。
标记-清除
Python采用了标记-清除
(Mark and Sweep)算法,解决容器对象可能产生的循环引用问题。
在进行垃圾回收时的步骤:
- 标记阶段:遍历所有的对象,如果是可达的,也就是还有引用指向它,那么就标记该对象为可达
- 清楚阶段:再次遍历对象,如果发现某个对象没有标记为可达,则就将其回收
标记清除算法作为Python的辅助垃圾收集技术主要处理的是一些容器对象,比如list、dict、tuple,instance等,因为对于字符串、数值对象是不可能造成循环引用的问题。
Python会使用一个双向链表将这些容器对象组织起来。不过标记清除算法也有明显的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只有一小部分的活动对象也要扫描所有对象。
标记-清除
是为了解决循环引用问题。可以包含其他对象引用的容器对象都可讷讷个产生循环引用,为此,在申请内存时,所有容器头部会加上PyGC_Head来实现标记-清除
机制。
任何一个Python对象都分为两部分:PyGC_Head + 对象本身数据
而在对象申请内存的时候,实际申请的内存数量已经加上了PyGC_Head的大小
分代回收
在循环引用对象的回收中,整个应用程序会被暂停,为了减少应用程序暂停的时间,Python通过分代回收
(Generational Collection)以空间换时间的方法提高垃圾回收效率。
对于陈新股,存在一定比例的内存块的生存周期比较短;而剩下的内存块,生存周期会比较长,甚至会从程序开始一直持续到程序结束。生存期较短对象的比例通常在80%~90%之间,这种思想简单点说就是:对象存在的时间越长,越可能不是垃圾,应该越少去收集。这样在执行``标记-清除`算法时可以有效减少遍历的对象数,从而提高垃圾回收的速度。
原理
Python gc给对象定义了三种代(0,1,2),每一个新生对象在generation zero中,如果他在一轮gc扫描汇总活了下来,那么它将被移至generation one,再继续进行较少的扫描,如果它又活过了一轮gc,它又将被移至generation two,继续进行更少次数的扫描。
触发扫描
当某一代中被分配的对象与被释放的对象之差达到某一阈值的时候,就会触发gc对某一代的扫描。
注意
当某一代的扫描被触发时,比该代年轻的代也会被扫描。简单来说就是当代2的gc扫描被触发了,那么代0、代1也将被扫描,如果代1的gc扫描触发了,那么代0也会被扫描
触发条件
- 被引用为0时,立即回收当前对象
- 达到了垃圾回收的阈值,触发标记-清除
- 手动调用gc.collect()
- Python虚拟机退出的时候
总结
总体来说,在Python中,主要通过引用计数
进行垃圾回收;通过标记-清除
解决容器对象可能产生的循环引用问题;通过分代回收
以空间换时间的方法提高垃圾回收效率。