剖析 Python 面试知识点(二)- 内存管理和垃圾回收机制

1. 内存管理

Python 中一切皆对象,对象又可以分为可变对象和不可变对象。二者可以通过原地修改,如果修改后地址不变,则是可变对象,否则为不可变对象,地址信息可以通过id()进行查看。

>>> a = 10
>>> id(a)
4339392960
>>> a =11
>>> id(a)
4339392992
>>> a = [1, 2]
>>> id(a)
4342877192
>>> a.append(3)
>>> a
[1, 2, 3]
>>> id(a)
4342877192

Python 有内存池机制,Pymalloc机制,用于对内存的申请和释放管理。先来看一下为什么有内存池:

当创建大量消耗小内存的对象时,c中频繁调用new/malloc会导致大量的内存碎片,致使效率降低。

内存池的概念就是预先在内存中申请一定数量的,大小相等的内存块留作备用,当有新的内存需求时,就先从内存池中分配内存给这个需求,不够了之后再申请新的内存。这样做最显著的优势就是能够减少内存碎片,提升效率。

查看源码,可以看到 Pymalloc 对于小的对象,Pymalloc会在内存池中申请空间,一般是少于236kb,如果是大的对象,则直接调用 new/malloc 来申请新的内存空间。

有了内存的创建,那就需要回收,垃圾回收机制,也是 Python 面试当中必问的一个知识点,接下来看看垃圾回收机制是什么?

2 垃圾回收机制

垃圾回收机制,Python 采用 GC 作为自动内存管理机制,GC要做的有2件事,一是找到内存中无用的垃圾对象资源,二是清除找到的这些垃圾对象,释放内存给其他对象使用。

如何实现上述2点了,Python 采用了 引用计数 为主, 标志清除和分代回收 为辅测策略。

2.1 引用计数

查看源码,每一个对象,在源码里就是一个结构体表示,都会有一个计数字段.

typedef struct_object {
 int ob_refcnt;
 struct_typeobject *ob_type;
} PyObject;

PyObject是每个对象必有的内容,其中ob_refcnt就是做为引用计数。当一个对象有新的引用时,它的ob_refcnt就会增加,当引用它的对象被删除,它的ob_refcnt就会减少。 一旦对象的引用计数为0,该对象立即被回收,对象占用的内存空间将被释放。

此算法的优点和缺点都是非常明显的: 优点:

  • 简单
  • 实时性:一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。

缺点:

  • 需要额外的空间维护引用计数。
  • 不能解决对象的循环引用。(主要缺点)

接下来说一下什么是循环引用:

A和B相互引用而且没有外部引用A与B中的任何一个。也就是对象之间互相应用,导致引用链形成一个环。

>>>>>>a = { } #对象A的引用计数为 1
>>>b = { } #对象B的引用计数为 1
>>>a['b'] = b  #B的引用计数增1
>>>b['a'] = a  #A的引用计数增1
>>>del a #A的引用减 1,最后A对象的引用为 1
>>>del b #B的引用减 1, 最后B对象的引用为 1

执行 del 后,A、B对象已经没有任何引用指向这两个对象,但是这两个对象各包含一个对方对象的引用,虽然最后两个对象都无法通过其它变量来引用这两个对象了,这对GC来说就是两个非活动对象或者说是垃圾对象。理论上是需要被回收的。 按上面的引用计数原理,要计数为0才会回收,但是他们的引用计数并没有减少到零。因此如果是使用引用计数法来管理这两对象的话,他们并不会被回收,它会一直驻留在内存中,就会造成了内存泄漏(内存空间在使用完毕后未释放)。

为了解决对象的循环引用问题,Python 引入了标记清除和分代回收两种GC机制。

2.2 标记清除

标记清除主要是解决循环引用问题。

标记清除算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。 它分为两个阶段:第一阶段是标记阶段,GC会把所有的 活动对象 打上标记,第二阶段是把那些没有标记的对象 非活动对象 进行回收。那么GC又是如何判断哪些是活动对象哪些是非活动对象的呢?

对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。

在上图中,我们把小黑圈视为全局变量,也就是把它作为root object,从小黑圈出发,对象1可直达,那么它将被标记,对象2、3可间接到达也会被标记,而4和5不可达,那么1、2、3就是活动对象,4和5是非活动对象会被GC回收。

标记清除算法作为 Python 的辅助垃圾收集技术主要处理的是容器对象(container,上面讲迭代器有提到概念),比如list、dict、tuple等,因为对于字符串、数值对象是不可能造成循环引用问题。Python使用一个双向链表将这些容器对象组织起来。

Python 这种简单粗暴的标记清除算法也有明显的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。

2.3 分代回收

分代回收是一种以空间换时间的操作方式。

Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。同时,分代回收是建立在标记清除技术基础之上。

分代回收同样作为Python的辅助垃圾收集技术处理容器对象

『剖析Python面试知识点』完整内容请查看 :

https://gitbook.cn/gitchat/activity/5c8e364590020a6262806c8d

更多精彩文章请关注公众号: 『天澄技术杂谈』

天澄技术杂谈

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券