概述
前言
想要了解python,就必须要了解Python的内存管理机制,不然我们就会经常踩进一些莫名其妙的坑!
Python的内存管理机制共分为三部分:1、引用计数 2、垃圾回收 3、内存池机制
在了解以上三部分内容之前,我们先来了解一下python的变量与对象:
我们可以简单的把python的变量理解为指向对象的一个指针,这个指针指向了对象在内存中的真正存储位置,从而通过这个变量指针获取对象的值。而python对象是类型已知的、明确的内存数据或者内存空间,内存空间中存储了它们所表示的值,如果不理解的话,我们举个例子:
>>> a = '123'
这里,真正的对象是’123’字符串,而a只是指向这个字符串内存空间的一个指针,通过赋值’=’,我们把变量a和对象’123’之间建立了连接关系或者映射关系,就是我们所说的"引用",所以我们可以通过这个指针a来获取对象’123’的值。
从中,我们也可以看出变量名其实是没有类型的,类型是属于对象的,由于变量引用了对象,所以变量的类型就是所引用对象的类型!
我们可以通过python的内置函数id(),来查看变量所引用对象的id,也就是内存地址:
>>> a = 1
>>> b = a
>>> print(id(a), id(b))
2193993458056
同时也可以使用内置关键字 is 来判断两个变量是否引用同一个对象:
>>> a = '123'
>>> b = '123'
>>> print(a is b)
True
一、引用计数
接下来我们来介绍一下python的引用计数:
在python中,每个对象都会包含一个头部信息,这个头部信息包括:类型标识符和引用计数器!
查看对象的引用计数可以调用 sys.getrefcount():
>>> import sys
>>> a = [1, 2]
>>> sys.getrefcount(a)
2
>>> b = a
>>> sys.getrefcount(a)
3
>>> sys.getrefcount(b)
3
这里需要注意的是,第一次把某个引用作为参数传递给 sys.getrefcount() 时,会临时创建一个该参数的临时引用,所以我们看到第一次调用时发现比实际的多1
引用计数增加的方式:
1、对象的创建
>>> a = [1, 2]
>>> sys.getrefcount([1, 2])
2
2、引用的赋值
>>> b = a
>>> sys.getrefcount([1, 2])
3
3、作为容器对象的一个元素
>>> c = [1, 2, [1, 2]]
>>> sys.getrefcount([1, 2])
4
4、作为参数传递给函数
>>> foo(a)
>>> sys.getrefcount([1, 2])
5
引用计数减少的方式:
1、显示得销毁对象引用
>>> del a
>>> sys.getrefcount([1, 2])
4
2、该对象的引用被赋值了其它对象
>>> b = '12'
>>> sys.getrefcount([1, 2])
3
3、从容器对象中移除
>>> c.remove([1, 2])
>>> sys.getrefcount([1, 2])
2
4、引用离开作用域,比如函数foo()结束返回
二、 垃圾回收
python垃圾回收的原理:
当python对象的引用计数为0时,python解释器会对这个对象进行垃圾回收。
但需要注意的是在垃圾回收的时候,python不能进行其它任务,所以如果频繁的进行垃圾回收将大大降低python的工作效率,因此,python只会在特定的条件下自动进行垃圾回收,这个条件就是"阈值",在python运行过程中,会记录对象的分配和释放次数,当这两个次数的差值高于阈值的时候,python才会进行垃圾回收。
查看阈值:
>>> import gc
>>> gc.get_threshold()
(700, 10, 10)
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)
第一个10代表每10次0代的垃圾回收才会触发1次1代的垃圾回收,每10次1代的垃圾回收才会触发1次2代的垃圾回收。当然,也可以手动垃圾回收:
>>> gc.collect()
2
三、 python 内存池机制
为了避免频繁的申请和释放内存,python的内置数据类型,数值、字符串,查看python源码可以看到数值缓存范围为 -5 ~ 257
#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS 257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS 5
#endif
对于 -5 ~ 257 范围内的数值,创建之后python会把其加入到缓存池中,当再次使用时,则直接从缓存池中返回,而不需要重新申请内存,如果超出了这个范围的数值,则每次都需要申请内存。下面看个例子:
>>> a = 66
>>> b = 66
>>> id(a) == id(b)
True
>>> x = 300
>>> y = 300
>>> id(x) == id(y)
False
字符串的 intern 机制:
Python 解释器中使用了 intern (字符串驻留)的技术来提高字符串效率,所谓 intern 机制,指的是:字符串对象仅仅会保存一份,放在一个共用的字符串储蓄池中,并且是不可更改的,这也决定了字符串时不可变对象。
机制原理:
实现 Intern 机制的方式非常简单,就是通过维护一个字符串储蓄池,这个池子是一个字典结构,如果字符串已经存在于池子中就不再去创建新的字符串,直接返回之前创建好的字符串对象,如果之前还没有加入到该池子中,则先构造一个字符串对象,并把这个对象加入到池子中去,方便下一次获取。
但并非全部的字符串都会采用 intern 机制,只有包括下划线、数字、字母的字符串才会被 intern,同时字符数不能超过20个,因为如果超过20个字符的话,Python 解释器就会认为这个字符串不常用,不用放入字符串池子中。
最后
以上就是迅速康乃馨为你收集整理的【Python】之内存管理机制前言一、引用计数二、 垃圾回收三、 python 内存池机制的全部内容,希望文章能够帮你解决【Python】之内存管理机制前言一、引用计数二、 垃圾回收三、 python 内存池机制所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复