2022年 11月 9日

【Python】之内存管理机制

前言


想要了解python,就必须要了解Python的内存管理机制,不然我们就会经常踩进一些莫名其妙的坑!

Python的内存管理机制共分为三部分:1、引用计数 2、垃圾回收 3、内存池机制

在了解以上三部分内容之前,我们先来了解一下python的变量与对象:

在这里插入图片描述
我们可以简单的把python的变量理解为指向对象的一个指针,这个指针指向了对象在内存中的真正存储位置,从而通过这个变量指针获取对象的值。而python对象是类型已知的、明确的内存数据或者内存空间,内存空间中存储了它们所表示的值,如果不理解的话,我们举个例子:

>>> a = '123'
  • 1

这里,真正的对象是’123’字符串,而a只是指向这个字符串内存空间的一个指针,通过赋值’=’,我们把变量a和对象’123’之间建立了连接关系或者映射关系,就是我们所说的”引用”,所以我们可以通过这个指针a来获取对象’123’的值。

从中,我们也可以看出变量名其实是没有类型的,类型是属于对象的,由于变量引用了对象,所以变量的类型就是所引用对象的类型!

我们可以通过python的内置函数id(),来查看变量所引用对象的id,也就是内存地址:

>>> a = 1
>>> b = a
>>> print(id(a), id(b))
2193993458056
  • 1
  • 2
  • 3
  • 4

同时也可以使用内置关键字 is 来判断两个变量是否引用同一个对象:

>>> a = '123'
>>> b = '123'
>>> print(a is b)
True
  • 1
  • 2
  • 3
  • 4

一、引用计数


接下来我们来介绍一下python的引用计数:

在python中,每个对象都会包含一个头部信息,这个头部信息包括:类型标识符和引用计数器!

查看对象的引用计数可以调用 sys.getrefcount():

>>> import sys
>>> a = [1, 2]
>>> sys.getrefcount(a)
2
>>> b = a
>>> sys.getrefcount(a)
3
>>> sys.getrefcount(b)
3
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这里需要注意的是,第一次把某个引用作为参数传递给 sys.getrefcount() 时,会临时创建一个该参数的临时引用,所以我们看到第一次调用时发现比实际的多1

引用计数增加的方式:

1、对象的创建

>>> a = [1, 2]
>>> sys.getrefcount([1, 2])
2
  • 1
  • 2
  • 3

2、引用的赋值

>>> b = a
>>> sys.getrefcount([1, 2])
3
  • 1
  • 2
  • 3

3、作为容器对象的一个元素

>>> c = [1, 2, [1, 2]]
>>> sys.getrefcount([1, 2])
4
  • 1
  • 2
  • 3

4、作为参数传递给函数

>>> foo(a) 
>>> sys.getrefcount([1, 2]) 
5
  • 1
  • 2
  • 3

引用计数减少的方式:

1、显示得销毁对象引用

>>> del a 
>>> sys.getrefcount([1, 2]) 
4
  • 1
  • 2
  • 3

2、该对象的引用被赋值了其它对象

>>> b = '12' 
>>> sys.getrefcount([1, 2]) 
3
  • 1
  • 2
  • 3

3、从容器对象中移除

>>> c.remove([1, 2]) 
>>> sys.getrefcount([1, 2]) 
2
  • 1
  • 2
  • 3

4、引用离开作用域,比如函数foo()结束返回

二、 垃圾回收


python垃圾回收的原理

当python对象的引用计数为0时,python解释器会对这个对象进行垃圾回收。

但需要注意的是在垃圾回收的时候,python不能进行其它任务,所以如果频繁的进行垃圾回收将大大降低python的工作效率,因此,python只会在特定的条件下自动进行垃圾回收,这个条件就是”阈值”,在python运行过程中,会记录对象的分配和释放次数,当这两个次数的差值高于阈值的时候,python才会进行垃圾回收。

查看阈值

>>> import gc 
>>> gc.get_threshold() 
(700, 10, 10)
  • 1
  • 2
  • 3

700就是垃圾回收启动的阈值,后面的两个10是什么呢?它们是python的垃圾分代回收机制。为了处理如list、dict、tuple等容器对象的循环引用问题,python引用了标记-清除和分代回收的策略。

标记清除

标记清除是一种基于追踪回收(tracing GC)技术实现的回收算法,它分为两个阶段:第一阶段是把所有活动对象打上标记,第二阶段是把没有标记的非活动对象进行回收,对象是否活动的判断方法是:从根对象触发,沿着”有向边”遍历所有对象,可达的对象就会被标记为活动对象,不可达的对象就是后面需要清除的对象,如下图

在这里插入图片描述
从根对象(小黑点)出发,1、2、3可达,4、5不可达,那么1、2、3就会被标记为活动对象,4、5就是会被回收的对象,这种方法的缺点是:每次清除非活动对象前都要扫描整个堆内存里面的对象。

分代回收

分代回收是一种以空间换时间的操作方式,python把所有对象的存货时间分为3代(0、1、2),对应着3个链表,新创建的对象会被移到第0代,当第0代的链表总数达到上限时,就会触发python的垃圾回收机制,把所有可以回收的对象回收,而不会回收的对象就会被移到1代,以此类推,第2代的对象是存活最久的对象,当然分代回收是建立在标记清除技术的基础上的。

现在回过头来分析之前的阈值:

>>> gc.get_threshold() 
(700, 10, 10)
  • 1
  • 2

第一个10代表每10次0代的垃圾回收才会触发1次1代的垃圾回收,每10次1代的垃圾回收才会触发1次2代的垃圾回收。当然,也可以手动垃圾回收:

>>> gc.collect() 
2
  • 1
  • 2

三、 python 内存池机制


为了避免频繁的申请和释放内存,python的内置数据类型,数值、字符串,查看python源码可以看到数值缓存范围为 -5 ~ 257

#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS           257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS           5
#endif
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

对于 -5 ~ 257 范围内的数值,创建之后python会把其加入到缓存池中,当再次使用时,则直接从缓存池中返回,而不需要重新申请内存,如果超出了这个范围的数值,则每次都需要申请内存。下面看个例子:

>>> a = 66
>>> b = 66
>>> id(a) == id(b)
True

>>> x = 300
>>> y = 300
>>> id(x) == id(y)
False
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

字符串的 intern 机制

Python 解释器中使用了 intern (字符串驻留)的技术来提高字符串效率,所谓 intern 机制,指的是:字符串对象仅仅会保存一份,放在一个共用的字符串储蓄池中,并且是不可更改的,这也决定了字符串时不可变对象。

机制原理:

实现 Intern 机制的方式非常简单,就是通过维护一个字符串储蓄池,这个池子是一个字典结构,如果字符串已经存在于池子中就不再去创建新的字符串,直接返回之前创建好的字符串对象,如果之前还没有加入到该池子中,则先构造一个字符串对象,并把这个对象加入到池子中去,方便下一次获取。

但并非全部的字符串都会采用 intern 机制,只有包括下划线、数字、字母的字符串才会被 intern,同时字符数不能超过20个,因为如果超过20个字符的话,Python 解释器就会认为这个字符串不常用,不用放入字符串池子中。