python高阶教程-对象管理与垃圾回收

本篇内容来自原创小册子《python高阶教程》,点击查看目录

问题引入

考虑如下代码,运行后输出为?

1
2
3
4
5
a = {1:[1,2,3]}
b = a.copy()
a[1][0] = 2
print(a)
print(b)

输出:

1
2
{1:[2,2,3]}
{1:[2,2,3]}

为什么b的输出不是{1:[1,2,3]}呢? 这要从python的对象管理说起。

基于引用的对象管理

python的对象是基于引用来管理的,每个对象维护一个引用计数器。

比如如下代码:

1
2
3
var1 = 'A'
var2 = var1
var3 = 'B'

运行后在内存中的分配如下:
20190905235000.png

可以看到var1 和 var2 的内容都是对象’A’, 但是两者共享一个对象。

copy模块

我们可使用copy模块中的函数来复制一个复杂对象,主要分为shallow copydeep copy两类

shallow copy 仅复制第一层,不递归复制。切片、copy模块的copy函数
deep copy 对子对象进行递归复制。Copy模块的deepcopy函数

20190905235030.png

20190905235042.png

上图是不同的函数复制后的效果,可以看到deep copy函数把字典对象里面嵌套的列表对象也完全复制过来了,但是shallow copy不复制嵌套对象。

垃圾的分代回收

有用的对象叫对象,无用的对象叫垃圾,有垃圾就要有回收机制,在python中垃圾回收是自动进行的。主要有如下规则:

  1. 所有对象分为0、1、2三代
  2. 某一代对象经历回收后依旧存活,归入下一代
  3. 一般上,10次0代回收后,配合1次1代回收;其余类推
  4. 0代回收的启动机制:内存分配数 – 删除数
    对于第3条,这个数字是可以查看的,有如下代码
    1
    2
    import gc
    gc.get_threshold()

输出:

1
(700, 10, 10)

元组表示的含义为:计数器达到700时启动0代对象的回收;每经过10次0代对象回收,额外对1代对象进行一次回收;每经过10次1代对象回收,额外对2代对象进行一次回收。

对于第4条,在python中维护一个计数器,用来统计内存分配与回收的个数,同时用来启动0代垃圾回收。有如下代码:

1
2
3
4
5
6
7
8
import gc
class A():
pass
print(gc.get_count())
a = A()
print(gc.get_count())
del a
print(gc.get_count())

输出:

1
2
3
(574,8,0)
(575,8,0)
(574,8,0)

可以看到经历了一次内存分配与删除后,输出元组的第一个数字经历了574-575-574的变化。不难看出,第二个数字代表距上一次1代垃圾回收已经过去了8个计数器的值,经过上一次2代垃圾回收已经过去了0个计数器的值。

具体的垃圾回收操作是找到引用计数为0的对象并销毁,但是,如果是如下代码呢?

1
2
3
4
a = []
b = []
a.append(b)
b.append(a)

这里a和b循环引用,内存中对象的引用计数永远不为0,该如何进行垃圾回收呢?

循环引用回收-标记清除

标记清除主要有以下规则:

  1. 通过引用计数的副本寻找root object集合
    1.1 从表头出发,碰到引用就将目标对象的引用计数值减1
    1.2 不为0的对象移到root object
    1.3 root object引用的对象移到root object
  2. 链表1中维护root object集合,成为root链表
  3. 链表2中维护剩下的对象,成为unreachable链表
  4. 对unreachable链表的对象进行垃圾回收

用一个例子来解释

20190905235101.png

在该例中,共有四个对象,分别是’A’, ‘B’, ‘C’, ‘D’, 引用计数都为1, 但是’A’引用了’C’,

‘B’引用了’D’。

标记清除操作开始。首先,复制对象的引用计数。然后,只要对象间有引用,不管是不是循环引用,被引用对象的计数值都要减1. 如A引用了C,C的引用值减1,B引用了D,D的引用值减1,其余类推。最后,把不为0的对象加入root链表,root列表引用的对象加入root链表,其他加入unreachable链表。在此例中,A首先被加入root链表,被A引用的C也被加入root链表。

标记清除操作结束后,对unreachable链表中的对象进行垃圾回收。

0%