day05 垃圾回收机制(超小白讲解)

垃圾回收机制

在学习这个抽象概念前,老习惯,灵魂二问

什么是?为什么要有?

引言:在程序运行到变量定义时,会在内存空间中存放变量值,然而内存空间是有限的,变量是无限的。

Q:如何在有限的内存里存里存放无限的变量呢?

A:不存在的,但是可以对变量赋值的过程进行优化。

由此诞生了垃圾回收机制

1.什么是垃圾回收机制

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

Q:那么什么是垃圾呢?

A:可以用一段代码来解释:

a = 10
#这个过程在我们看来是我们给了a这个变量传了一个值为10
#而在计算机底层是这样的,在定义变量a时,在内存里开辟了一个空间,这个空间用来存放10,变量a和值10存在绑定关系,这种绑定关系叫做引用计数,此时10的引用计数为1。
a = 20
#这个时候我们重新在内存里开辟了一个空间,在里面存放了一个值为20,并将a和10的绑定关系解除,把20和a绑定。这个时候10的引用计数为0,20的引用计数为1,这样在内存里存在了一个无法访问的变量值,但是它会一直占用内存空间,这就是垃圾。

2.为什么要有垃圾回收机制

在程序的运行中,会占用大量的内存空间,时间长了就会存在许多没有用的垃圾占用大量的内存空间,如果不及时清理会造成内存使用殆尽(内存溢出),导致程序崩溃。

3.垃圾回收机制扩展

这时候可能会觉得哪里不对劲,引用计数的变化是在每一个变量定义时都会记录,存在非常大的效率问题。就像你在写作业,一个人写一次老师就检查一次,一个人还好,如果全校每个人的作业写完都是这个老师检查过去,就非常麻烦。

而且,引用计数还存在一个非常致命的缺陷,就是循环引用

# 如下我们定义了两个列表,简称列表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

# l1与l2之间有相互引用
# l1 = ['xxx'的内存地址,列表2的内存地址]
# l2 = ['yyy'的内存地址,列表1的内存地址]
l1 ['xxx', ['yyy', [...]]]
l2 ['yyy', ['xxx', [...]]]
l1[1][1][0]
>>>'xxx'
#到这里我们的准备工作结束了,我们可以看一下两个列表的引用计数,l1列表的内容,被l1引用了1次,被l2中第二个元素引用了一次,此时l1列表的内容引用计数为2
del l1# 列表l1的引用计数减少1,现在引用计数为1
del l2# 列表l2的引用计数减少1,现在引用计数为1
#但是此时我们却无法访问l1列表内的内容,本该被当做垃圾删除,但是他的引用计数为1,被l2列表间接引用。这个时候引用计数在这种垃圾身上就失效了。

4.标记清除

标记清除是为了弥补引用计数的缺陷,解决容器对象(list,set,dict...)内循环引用的问题。

在了解这个之前,我们可以更深入的看一下变量定义的过程

x = 10

是在栈区中有变量名x,还有x和10的关联关系,在堆区里存放着值10

当我们执行x=y时,内存中栈区和堆区的变化:

Q:标记清除是和引用计数一起工作的吗?又是怎么工作的呢?

A:标记清除是当内存即将被应用程序占满时,会把整个程序停下来,开始执行标记清除,此时分为两步:

第一步:标记

​ 标记的过程其实就是,遍历所有的GC Roots对象(栈区中的所有内容或者线程都可以作为GC Roots对象),然后将所有GC Roots的对象可以直接或间接访问到的对象标记为存活的对象,其余的均为非存活对象,应该被清除。

第二步:

​ 清除 清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。

这里再回到刚刚标记清除里的代码,循环引用时的情况,我们del l1和del l2时会在栈区里把l1和堆区内的l1的内容的元素清除绑定关系,这个时候只有在堆区内两个列表的元素在互相引用,此时就被标记了。然后所有类似的没有和栈区内的变量名有直接引用关系的都会被清除!

5.分代回收

标记清除解决了引用计数在循环引用上的缺陷,而分代回收就是为了解决引用计数的第二个缺陷,效率问题。

分代:可以做个比喻,在一个班里有40个学生,每天教一份作业,每次都检查就会很麻烦,所以我们可以在多次检查后,把这40个学生分为几部分:

第一部分:好学生,每次都交,就一周检查一次把

第二部分:良好的学生,偶尔不交,就三天检查一次把

第三部分:差生,总是不交,每次都得检查

在这样区分后,每次检查作业的效率就提高了。在程序中,每一部分就分为每一代,根据每一代的性质不同,对他们的扫描频率就不同。

即便这样,也存在缺陷,比如一个学生刚当上好学生,他就不交作业了,但是我们一周才能查出来。在程序中就是一个被放入最高代的变量,他刚进去变量绑定关系就解除了,但是这一代的扫描频率很低,很久才能把它查出来。

原文地址:https://www.cnblogs.com/hz2lxt/p/12420309.html