python内存管理、垃圾回收机制(总结)

2021-04-12 03:27

阅读:665

标签:move   无法   要求   经历   原来   阈值   特定   append   类型   

内存管理机制:引用计数、垃圾回收、内存池机制

 

1.变量与对象
变量:通过变量指针引用对象,变量指针指向具体对象的内存地址,最终这个变量取的是对象的值
个人理解:变量中保存的是数据地址,这里的对象即是指数据

对象:类型已知,每个对象都包含头部信息
头部信息中存的是对象的类型标识符和引用计数器 # 对象即数据 类型即数据类型 引用计数即有多少变量指向这个数据
变量名是没有类型的,平时得到的变量类型都是对象的类型,因为变量是引用的对象


引用所指:通过is判断两个引用所指的是否是同一对象
比如:
a = 1
b = a
print(a is b) #结果是true,a和b的地址相同

 

关于引用:
1)python缓存了整数和短字符串,整数和短字符串创建后在内存中只有一份,之后如果再创建相同的整数或短字符串,实际上都指向第一次创建的这个整数和短字符串

比如:
a = 1
b = 1
print(a is b) #结果是true,a和b的地址相同


2)长字符串、列表等其他对象,通过赋值语句,可以创建多个新的相同对象【数据的值相同,数据地址不同】

3)长字符串和短字符串的区分:由数字字母下划线组成,不含其他字符的,就是短字符串,其他是长字符串

 

2.引用计数
python中,每个对象都有指向该对象的引用总数--引用计数
查看对象的引用计数:sys.getrefcount()

 

1)引用计数增加:
1-1)赋值语句会创建一个引用。
前提是这个数据之前没有被创建 或者这个数据之前被创建了 但不是整数和短字符串

1-2)给函数传参会增加一个引用,比如
a = "123"
len(a),a把“123”的数据地址传递给了函数内部的参数,所以“123”会增加一个引用计数

1-3)作为列表或字典的元素,对象的引用会增加
前提是这个元素是一个整数或者短字符
增加的意思是,在这之前对象已被创建
比如:
a = 1 # 创建了对象,引用计数为1
b = [1, 2, 3] # 列表中使用了1, 1的引用计数增加1个,变成2


2)引用计数减少:

2-1)对象的别名被显示销毁。别名指的是变量
比如
a=1
del a

2-2)对象的别名改变了引用
比如
a = 1 # 1的引用为1
b = a # 1的引用为2
b = 3 # b的变成了对3的引用,1的引用计数减少1

2-3)对象从一个窗口对象中移除,或者窗口对象本身被销毁

a = [1, 2, 123] # 假设这里第一次创建123,引用计数为1
b = 123 # 123的引用计数+1 变成2
a.remove(123) # 123的引用计数减少1 恢复为1

2-4)一个本地引用离开了它的作用域
比如需要传参的函数,在函数执行完以后,参数所指向的数据,它的引用计数会减少1

 

3.垃圾回收

当python中的对象原来越多时,占据越来越多内存,会启动垃圾回收,清除没用的对象

1)原理
当python某个对象的引用计数变为0后,说明没有任何引用指向该对象,该对象就需要被回收。

当垃圾回收启动的时候,python扫描到这个对象的引用计数为0,就会把这个数据清空,腾出内存空间

2)垃圾回收何时启动
启动垃圾回收时,python不能进行其他的任务,所以不能进行太频繁的垃圾回收

python只在特定条件下自动启动垃圾回收

当Python运行时,会记录其中分配对象(object allocation)和取消分配对象(object deallocation)的次数。当两者的差值高于某个阈值时,垃圾回收才会启动

In [93]: import gc

In [94]: gc.get_threshold()  #gc模块中查看阈值的方法
Out[94]: (700, 10, 10)


3)阈值分析:

  700即是垃圾回收启动的阈值;

  每10次0代垃圾回收,会配合1次1代的垃圾回收;而每10次1代的垃圾回收,才会有1次的2代垃圾回收;

当然也是可以手动启动垃圾回收: 

In [95]: gc.collect() #手动启动垃圾回收
Out[95]: 2


4)分代回收

Python将所有的对象分为0,1,2三代;

所有的新建对象都是0代对象;

当某一代对象经历过垃圾回收,依然存活,就被归入下一代对象。


3.1垃圾回收的理解
1)正常情况下垃圾回收启动时,如果扫描到对象的引用计数为0,那么就会清除该对象,回收这个对象占据的内存空间

2)特殊情况:存在循环引用导致有些内存没有办法回收。如下

a = ["1"] # a列表的引用计数为1
b = ["2"] # b列表的引用计数为1
a.append(b) # b列表的引用计数为2,a=["1", b]
b.append(a) # a列表的引用计数为2,b=["1", a]
del a # a的引用计数为1
del b # b的引用计数为1

执行了del a 和del b,两个变量名 都被删除了,很显然想要的结果是直接让这两个列表的引用变为0,可以进行回收
但是变量名a b 虽然没有了,可a列表和b列表的引用计数却都还剩1,达不到回收这两个列表的要求
而外界已没有指向这两个列表的变量名了,外界无法访问,则无法再通过del去把这两个列表的引用计数变为0
导致这两个内存无法回收【内存泄漏】

为了解决循环引用带来的问题,有了分代回收机制

 

3)分代回收机制
分代回收中,如果对象的引用计数为0,那么它所占的空间就会被python解释器回收


第一种理解:根据对象的权重把对象移入更高级的一代

分代回收的核心思想是:在多次扫描的情况下,都没有被回收的变量,GC机制就会认为,该变量是常用变量,GC对其扫描的频率会降低

具体实现原理如下:

分代指的是根据存活时间来为变量划分不同等级(也就是不同的代)

新创建的对象,放到新生代这个等级中,假设每隔1分钟扫描新生代一次,如果发现对象依然被引用,那么该对象的权重(权重本质就是个整数)加一, 当对象的权重大于某个设定得值(假设为3),会将它移动到更高一级的青春代,
青春代的gc扫描的频率低于新生代(扫描时间间隔更长),假设5分钟扫描青春代一次,这样每次GC需要扫描的变量的总个数就变少了,节省了扫描的总时间
接下来,青春代中的对象,也会以同样的方式被移动到老年代中。也就是等级(代)越高,被垃圾回收机制扫描的频率越低


第二种理解:每一代中的对象达到一定数量时进行扫描

a.python中每创建一个对象,就会把对象添加到一个特殊的“链表”,这个链表称为“零代链表”
当“零代链表”中的对象个数达到一定阈值,python解释器就会对这个“零代链表”进行一次“扫描”,查找列表中是否存在循环引用【互相引用】的对象,如果发现有互相引用的对象,就把这些对象的引用计数-1
此时,如果某些对象的引用计数变为0,python解释器就会回收其内存空间,如果对象的引用计数仍不为0,就把此时存活的对象迁移到下一代列表中

b.同样,python解释器会在一定条件下扫描“一代链表”,判断是否存在相互引用,存在就把这些对象的引用计数-1
此时如果某些对象的引用计数变为0,就回收对应的内存,如果对象的引用计数仍然不为0,就把对象移到“二代链表”中

c.扫描“二代链表”的操作同理,最后对象的引用计数仍不为0,则会把存活的对象再迁移到一个新的特殊内存空间

此时重新进行零代链表-》一代链表-》二代链表的循环扫描


4)标记-清除机制
为了解决误删的情况,比如
a = ["1"] # a列表的引用计数为1
b = ["2"] # b列表的引用计数为1
a.append(b) # b列表的引用计数为2,a=["1", b]
b.append(a) # a列表的引用计数为2,b=["1", a]
del a # a的引用计数为1

在进行分代回收扫描的时候,发现a列表和b列表存在相互引用的情况,于是对这两个列表的引用计数都进行减1
结果a列表的引用计数变为了0,然后a列表的内存被回收了,b列表未被回收,但是b列表中的元素还有对a列表的引用
此时a列表已经被回收,导致b列表中的a元素没有用了,所以实际上此时不应回收a列表的内存,于是有了“标记-清除”机制


“标记-清除”机制:
a.检测链表中相互引用的对象,让它们的引用计数-1
b.然后把所有对象分为两组:死亡组 存活组
引用计数为0的对象进入死亡组
引用计数不为0的进入存活组
c.此时分析存活组,如果对象存活,则其内部对象也必须存活
如果发现内部对象死亡,就想办法让其复活,这样就解决了误删的问题


5)小结:
python采用的引用计数为主,分代回收和标记清除为辅的垃圾回收机制,三者共同维护python程序占用的内存空间

 

 


4.内存机制

python内存管理、垃圾回收机制(总结)

标签:move   无法   要求   经历   原来   阈值   特定   append   类型   

原文地址:https://www.cnblogs.com/come202011/p/13356765.html


评论


亲,登录后才可以留言!