[新星计划] Python内存管理 | 引用计数垃圾回收内存池机制
Posted mycpen
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[新星计划] Python内存管理 | 引用计数垃圾回收内存池机制相关的知识,希望对你有一定的参考价值。
文章目录
系列文章
https://blog.csdn.net/cpen_web/category_11089219.html
Python内存管理三大块
○ 引用计数
○ 垃圾回收
○ 内存池
Python的内存管理以引用计数为主,垃圾回收为辅,还有个内存池
Python动态类型
○ 对象是储存在内存中的实体
○ 我们在程序中写的对象名,只是指向这一对象的引用(reference)
○ 引用和对象分离,是动态类型的核心
○ 引用可以随时指向一个新的对象(内存地址会不一样)
● 引用计数
在Python中,每个对象都有存有指向该对象的引用总数,即引用计数(reference count)
引用计数器原理
○ 每个对象维护一个 ob_ref 字段,用来记录该对象当前被引用的次数 每当新的引用指向该对象时,它的引用计数ob_ref加1
○ 每当该对象的引用失效时计数ob_ref减1
○ 一旦对象的引用计数为0,该对象可以被回收,对象占用的内存空间将被释放。 它的缺点是需要额外的空间维护引用计数,这个问题是其次的
○ 最主要的问题是它不能解决对象的“循环引用”
# 示例
# a = 1 , b = 1 ,1的引用计数为2(保存它被引用的次数)
# a = 2 , b = 3 , 1的引用计数为0(内存里面不需要它了,回收销毁,这块对象被回收了,对象占用的内存空间将被释放)
获取引用计数: getrefcount()
○ 当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用。因此,getrefcount()所得到的结果,会比期望的多1
# 示例
from sys import getrefcount # 导入模块
a = [1,2,3]
print(getrefcount(a)) # 获取对象a的引用计数 , 结果为2
增加引用计数
○ 当一个对象A被另一个对象B引用时,A的引用计数将增加1
减少引用计数
○ del删除或重新引用时,引用计数会变化(del只是删除引用)
# 示例
from sys import getrefcount
a = [1,2,3] # 真实引用计数:1
b = a # 真实引用计数:2
c = [a,a] # 真实引用计数:4
del c[0] # del删除引用 引用计数 - 1 ; 真实引用计数: 3
print(c) # c 是列表对象 输出为 [[1, 2, 3]]
print(getrefcount(a)) # 引用计数为4,真实引用计数为3
循环引用的情况
x = []
y = []
x.append(y)
y.append(x)
○ 对于上面相互引用的情况,如果不存在其他对象对他们的引用,这两个对象所占用的内存也还是无法回收,从而导致内存泄漏
# 示例1
>>> x = [1]
>>> y = [2]
>>> x.append(y)
>>> x
[1, [2]]
>>> y.append(x)
>>> y
[2, [1, [...]]] # 注:发生死循环
# 示例2
>>> from sys import getrefcount
>>> x = ["x"]
>>> y = ["y"]
>>> getrefcount(x)
2
>>> getrefcount(y)
2
>>> x.append(y)
>>> getrefcount(x)
2
>>> getrefcount(y)
3
>>> y.append(x)
>>> getrefcount(x)
3
>>> x
['x', ['y', [...]]]
>>> y
['y', ['x', [...]]]
>>> del x
>>> y
['y', ['x', [...]]]
>>> del y # del x;del y引用删除,这块内存区域获取不到了
引用计数机制的优点:
○ 简单
○ 实时性
引用计数机制的缺点:
○ 维护引用计数消耗资源
○ 循环引用时,无法回收
● 垃圾回收
回收原则
○ 当Python的某个对象的引用计数降为0时,可以被垃圾回收
gc机制
○ GC作为现代编程语言的自动内存管理机制,专注于两件事
○ 找到内存中无用的垃圾资源
○ 清除这些垃圾并把内存让出来给其他对象使用
GC彻底把程序员从资源管理的重担中解放出来,让他们有更多的时间放在业务逻辑上。但这并不意味着码农就可以不去了解GC,毕竟多了解GC知识还是有利于我们写出更健壮的代码
效率问题
○ 垃圾回收时,Python不能进行其它的任务。频繁的垃圾回收将大大降低Python的工作效率
○ 当Python运行时,会记录其中分配对象(object allocation)和取消分配对象(object deallocation)的次数。当两者的差值高于某个阈值时,垃圾回收才会启动
# 示例
>>> import gc
>>> print(gc.get_threshold())
(700, 10, 10) # 注:默认值
# 示例
>>> del x
>>> del y
>>> gc.collect()
2 # 删除了2个循环引用
>>> a = "x1 xx"
>>> b = "x1 xx"
>>> a = 1
>>> b = 2
>>> gc.collect()
0
三种情况触发垃圾回收
○ 调用gc.collect()
○ GC达到阀值时
○ 程序退出时
分代(generation)回收
这一策略的基本假设是:存活时间越久的对象,越不可能在后面的程序中变成垃圾
○ Python将所有的对象分为0,1,2三代
○ 所有的新建对象都是0代对象
○ 当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象
○ 垃圾回收启动时,一定会扫描所有的0代对象
○ 如果0代经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理
○ 当1代也经历了一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描
标记清除
标记-清除机制,顾名思义,首先标记对象(垃圾检测),然后清除垃圾(垃圾回收)
主要用于解决循环引用
○ 1.标记:活动(有被引用), 非活动(可被删除)
○ 2.清除:清除所有非活动的对象
● 缓冲池
整数对象缓冲池
○ 对于[-5,256] 这样的小整数,系统已经初始化好,可以直接拿来用。而对于其他的大整数,系统则提前申请了一块内存空间,等需要的时候在这上面创建大整数对象
# 示例:小整数而言 id都是一样的
>>> a = 777 # a和b不是一样的
>>> b = 777
>>> id(a) # 内存地址不同
140133545530064
>>> id(b) # 内存地址不同
140133545530384
>>> a = b = 777
>>> id(a)
140133545530480
>>> id(b)
140133545530480
>>> a = 1 # a和b是一样的
>>> b = 1 # python的整数对象缓冲池
>>> id(a)
140133544871840 # 内存地址一样
>>> id(b)
140133544871840 # 内存地址一样
>>> from sys import getrefcount
>>> getrefcount(a)
801
字符串缓存
○ 为了检验两个引用指向同一个对象,我们可以用is关键字。is用于判断两个引用所指的对象是否相同。
当触发缓存机制时,只是创造了新的引用,而不是对象本身
# 示例
>>> a = "xxx"
>>> b = "xxx"
>>> id(a)
140133545760616
>>> id(b)
140133545760616
>>> a = "xxx " # 注:特殊字符不能放到缓冲区
>>> b = "xxx "
>>> id(a)
140133545760672 # 内存地址不一样
>>> id(b)
140133545760728
>>> a = "xxx_" # 注:数字、字母、下划线的组合 放在字符串缓冲区
>>> b = "xxx_"
>>> id(a)
140133545760616 # 内存地址一样
>>> id(b)
140133545760616
>>> a = "hello world"
>>> b = "hello world"
>>> id(a) # 内存地址不一样
140133545242928
>>> id(b)
140133545242992
>>> a = "helloworld"
>>> b = "helloworld"
>>> id(a) # 内存地址一样
140133545243120
>>> id(b)
140133545243120
>>> a = "你好"
>>> b = "你好"
>>> id(a)
140612691332856
>>> id(b)
140612688894592
# 示例:对于乘法创建的字符 只会缓冲20个
>>> a = "x"*21
>>> b = "x"*21
>>> id(a) # 内存地址不一样
140133545742176
>>> id(b)
140133545742248
>>> a = "x"*20 # 内存地址一样
>>> b = "x"*20
>>> id(a)
140133545246768
>>> id(b)
140133545246768
注意
○ 这对于经常使用的数字和字符串来说也是一种优化的方案
字符串的intern机制
○ python对于短小的,只含有字母数字的字符串自动触发缓存机制。其他情况不会缓存
● 深拷贝与浅拷贝
浅拷贝
○ 拷贝第一层数据(地址)
# 示例
>>> a = "first":[1,2,3]
>>> b = a.copy() # 拷贝第一层数据(地址)
>>> a
'first': [1, 2, 3]
>>> b
'first': [1, 2, 3]
>>> id(a) # a、b引用变了
140133410603584
>>> id(b)
140133545741768
>>> a["second"] = "No.2"
>>> a
'first': [1, 2, 3], 'second': 'No.2'
>>> b
'first': [1, 2, 3]
>>> a["first"].append(4) # a、b里面的”first”引用 没有改变
>>> a # 拷贝第一层数据(地址)
'first': [1, 2, 3, 4], 'second': 'No.2'
>>> b
'first': [1, 2, 3, 4]
>>> id(a["first"]) # 第一层数据(地址) 内存地址相同
140133413100296
>>> id(b["first"])
140133413100296
深拷贝
○ 递归拷贝所有层的数据
# 示例
>>> a = "first":[1,2,3]
>>> import copy # 导入模块
>>> b = copy.deepcopy(a)
>>> id(a) # 内存地址不同
140133545248160
>>> id(b)
140133410604736
>>> a["second"] = "No.2"
>>> a["first"].append(4)
>>> a
'first': [1, 2, 3, 4], 'second': 'No.2' # 递归拷贝所有层的数据
>>> b
'first': [1, 2, 3] # 递归拷贝所有层的数据
小结
○ 数字和字符串、元组,不能改变对象本身,只能改变引用的指向,称为不可变数据对象(immutable object)
○ 列表、字典、集合可以通过引用其元素,改变对象自身(in-place change)。这种对象类型,称为可变数据对象(mutable object)
# 示例
>>> a=[1,2,3]
>>> b=[1,2,[4,5]] # 可变数据对象,有影响
>>> c = b[:]
>>> c
[1, 2, [4, 5]]
>>> b[2].append(7)
>>> b
[1, 2, [4, 5, 7]]
>>> c
[1, 2, [4, 5, 7]]
>>> b=[1,2,3] # 不可变数据对象,没有影响
>>> c = b[:]
以上是关于[新星计划] Python内存管理 | 引用计数垃圾回收内存池机制的主要内容,如果未能解决你的问题,请参考以下文章