目录
- 概述
- 1、什么是垃圾
- 2、什么是垃圾回收机制
- 3、垃圾回收机制的作用
- 引用计数
- 1、原理
- 2、引用介绍
- 3、引用计数增加的情况
- 4、引用计数减少的情况
- 5、引用计数存在的问题
- 标记-清除
- 1、标记
- 2、清除
- 3、作用
- 分代回收
概述
1、什么是垃圾
我们定义变量时,是将变量名与变量值关联的一个过程,其目的就是为了通过变量名去引用到我们所需要的变量值。当一个变量值不再绑定任何引用(变量名)时,我们就无法再访问到该变量值了,该变量值自然就是没有用的,我们就将其称之为垃圾。
2、什么是垃圾回收机制
垃圾回收机制(简称GC)是Python解释器自带一种机制,专门用来回收不可用的变量值所占用的内存空间。Python采用的是引用计数机制为主,标记清除和分代回收两种机制为辅的策略。
3、垃圾回收机制的作用
程序运行过程中会申请大量的内存空间,而对于一些无用的内存空间如果不及时清理的话会导致内存溢出,进而可能导致程序崩溃,因此管理内存是一件重要且繁杂的事情,而Python解释器自带的垃圾回收机制把程序员从繁杂的内存管理中解放出来。
引用计数
1、原理
引用计数法机制的原理是:每个对象维护一个ob_refcnt字段,用来记录该对象当前被引用的次数,每当新的引用指向该对象时,它的引用计数ob_refcnt加1,每当该对象的引用失效时计数ob_refcnt减1,一旦对象的引用计数为0,该对象立即被回收,对象占用的内存空间将被释放。
python里每一个东西都是对象,它们的核心就是一个结构体:PyObject:
typedef struct_object {
int ob_refcnt;
struct_typeobject *ob_type;
} PyObject;
PyObject是每个对象必有的内容,其中ob_refcnt就是做为引用计数。当一个对象有新的引用时,它的ob_refcnt就会增加,当引用它的对象被删除,它的ob_refcnt就会减少:
#define Py_INCREF(op) ((op)->ob_refcnt++) //增加计数
#define Py_DECREF(op) \ //减少计数
if (--(op)->ob_refcnt != 0)
;
else
__Py_Dealloc((PyObject *)(op))
当引用计数为0时,该对象生命就结束了。
2、引用介绍
引用有直接引用和间接引用两种,任何一种引用都会导致引用计数的增加。
x = 10 #对10的直接引用
l = [20, x] #对10的间接引用
为了更好的说明上述例子的直接引用和间接引用,我们来看看内存简图。
普通变量的引用如图:
其结果是x,y,z都是对10的直接引用。列表的引用如下图所示:
由图可知,列表中存放的并不是元素的值,而是索引和对应地址所构成的。这也就说明间接引用只存在于容器类型中。
3、引用计数增加的情况
- 对象被创建,例如 a=23
- 对象被引用,例如 b=a
- 对象被作为参数,传入到一个函数中,例如 func(a)
- 对象作为一个元素,存储在容器中,例如 list1=[10, a]
4、引用计数减少的情况
- 对象的别名被显式销毁,例如 del a
- 对象的别名被赋予新的对象,例如 a=24
- 一个对象离开它的作用域,例如 func函数执行完毕时,func函数中的局部变量(全局变量不会)
- 对象所在的容器被销毁,或从容器中删除对象
5、引用计数存在的问题
- 循环引用(交叉引用)
# 如下我们定义了两个列表,简称列表1与列表2,变量名l1指向列表1,变量名l2指向列表2
>>> l1 = ['222'] # 列表1被引用一次,列表1的引用计数变为1
>>> l2 = ['333'] # 列表2被引用一次,列表2的引用计数变为1
>>> l1.append(l2) # 把列表2追加到l1中作为第二个元素,列表2的引用计数变为2
>>> l2.append(l1) # 把列表1追加到l2中作为第二个元素,列表1的引用计数变为2
# l1与l2之间有相互引用
# l1 = ['222'的内存地址,列表2的内存地址]
# l2 = ['333'的内存地址,列表1的内存地址]
>>> l1
['222', ['333', [...]]]
>>> l2
['333', ['222', [...]]]
>>> del l1 # 列表1的引用计数减1,列表1的引用计数变为1
>>> del l2 # 列表2的引用计数减1,列表2的引用计数变为1
循环引用会导致:值不再被任何名字关联,但是值的引用计数并不会为0,应该被回收但不能被回收,如上例,按理,我们已经把 l1 和 l2 列表都删除了,但是由于 l1 和 l2 的引用计数并不为0,这将导致这两个的内存空间不会被回收。所以Python引入了“标记-清除” 来解决引用计数的循环引用的问题。
- 效率
基于引用计数的回收机制,每次回收内存,都需要把所有对象的引用计数都遍历一遍,这是非常消耗时间的。于是引入了分代回收来提高回收效率,分代回收采用的是用“空间换时间”的策略。
标记-清除
容器对象(比如:list,set,dict,class,instance)都可以包含对其他对象的引用,所以都可能产生循环引用。而“标记-清除”计数就是为了解决循环引用的问题。
标记/清除算法的做法是当应用程序可用的内存空间被耗尽的时,就会停止整个程序,然后进行两项工作,第一项则是标记,第二项则是清除。
1、标记
通俗地讲就是:
栈区相当于“根”,凡是从根出发可以访达(直接或间接引用)的,都称之为“有根之人”,有根之人当活,无根之人当死。
具体地:标记的过程其实就是,遍历所有的GC Roots对象(栈区中的所有内容或者线程都可以作为GC Roots对象),然后将所有GC Roots的对象可以直接或间接访问到的对象标记为存活的对象,其余的均为非存活对象,应该被清除。
2、清除
清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。
3、作用
基于上例的循环引用,当我们同时删除l1与l2时,会清理到栈区中l1与l2的内容以及直接引用关系这样在启用标记清除算法时,从栈区出发,没有任何一条直接或间接引用可以访达l1与l2,即l1与l2成了垃圾,于是l1与l2都没有被标记为存活,二者会被清理掉,这样就解决了循环引用带来的内存泄漏问题。
分代回收
- 分代回收是一种以空间换时间的操作方式,Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是3个链表,它们的垃圾收集频率随着对象存活时间的增大而减小。
- 新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。
- 同时,分代回收是建立在标记清除技术基础之上。分代回收同样作为Python的辅助垃圾收集技术处理那些容器对象。
上述内容只是简单的初学笔记,想要深入了解的可以看以下文章。