1. 不会被垃圾回收机制回收的
1. 小整数对象池
在Python中,会提前预留空间用于存储一些常用的数据,防止在程序执行过程中频繁的对这些数据申请和销毁内存导致性能降低
我们把这些数据称为小整数对象池,范围是[-5,256]
注意:想要验证的话必须要使用Python自带的IDE,其它IDE会对代码进行优化,得不到现象
展示一下:
# VScode
a = 1000
b = 1000
print(id(a))
print(id(b))
print(a is b)
"""
运行结果:
2264746932528
2264746932528
True
"""
# Python自带IDE
>>> a = 1000
>>> b = 1000
>>> id(a)
2950077528592
>>> id(b)
2950077528784
>>> a is b
False
>>> a = 256
>>> b = 256
>>> a is b
True
>>> a = 1.2
>>> b = 1.2
>>> a is b
False
只针对整数,浮点数是没有这个规则的
2. 大整数对象池
除了小整数对象池,就是大整数对象池
每一个大整数,都是一个新的对象
3. 字符串驻留机制(intern机制)
单个字母:开启intern机制,共用对象
>>> a = "a"
>>> b = "a"
>>> a is b
True
多个字符:只包含字母,数字,下划线,开启intern机制,共用对象
>>> a = "Aa_1"
>>> b = "Aa_1"
>>> a is b
True
除了上面的两种情况,都关闭intern机制,不共用对象
>>> a = "hello world"
>>> b = "hello world"
>>> a is b
False
2. 详解垃圾回收机制
相信大多数都接触过C语言,那么自然了解C语言对内存管理的自由度,内存的申请和销毁都由用户去操作,更加自由的同时,也会导致一些问题,对内存操作的不严谨会导致内存泄漏。而Python等此类编程语言自身就有一套管理内存,回收垃圾的机制,使得用户不用在此类问题上操心,其中Python的处理机制是以引用计数为主,标记-清除和分代回收的机制为辅的方式
接下来讨论一下这三种机制
1. 引用计数
Python中每一个对象都有一个引用计数,当增加一个引用时,计数+1,销毁一个引用时,计数-1,当某个对象的引用计数为0时,就是自动回收该对象(垃圾),释放空间
引用计数+1的4种情况:
import sys
#1. 创建
la = []
print(sys.getrefcount(la)) #2,包括la,la作为getrefcount()参数,这两种情况都指向空列表,退出该函数后,实际指向空列表的引用为1
#2. 赋值
b = la
print(sys.getrefcount(la)) #3,包括b,la作为getrefcount()参数,在原本的基础上+2得到3,同样退出该函数后,实际指向空列表的引用为2
#3. 放在容器中
c = [la,la,1]
print(sys.getrefcount(la)) #5,包括列表(有几个la,就有几个引用),la作为getrefcount()参数,在原本的基础上+3得到5,同样退出该函数后,实际指向空列表的引用为4
#4. 作为实参传递给函数
def test(n):
print(sys.getrefcount(n)) #7,包括test(la),n,la作为getrefcount()参数,在原本的基础上+3得到7,同样退出test和getrefcount函数后,实际指向空列表的引用为4
test(la)
print(sys.getrefcount(la)) #5,la作为getrefcount()参数,同样退出该函数后,实际指向空列表的引用为4
"""
2
3
5
7
5
"""
注意上面计算的都是对空列表[ ]的引用计数
引用计数-1的4种情况:
la = []
b = la
print(sys.getrefcount(la))
#1. 删除
del b
print(sys.getrefcount(la))
la = []
b = la
print(sys.getrefcount(la))
#2. 更改引用
b = 0
print(sys.getrefcount(la))
#3. 退出函数,例子就是getrefcount(la)
la = []
b = [la,1]
print(sys.getrefcount(la))
#4. 从容器中删除
b.remove(la)
print(sys.getrefcount(la))
引用计数的优点:
1. 简单,只用作加减法就行了
2. 将释放空间的时间平摊到平时,实时性较高,一旦引用计数为0,立马释放空间
引用计数的缺点:
1. 需要为每个对象分配计数空间,消耗资源
2. 循环引用,可以由另外两种机制解决
循环引用:
#创建两个类
class A():
def __init__(self) -> None:
self.i = 0
class B():
def __init__(self) -> None:
self.j = 0
a = A()
b = B()
a.i = B()
b.j = A()
del a
del b
导致这两个类互相引用,导致两个类的引用计数都为1,无法销毁,释放空间
2. 标记-清除
该机制主要是来解决循环引用的问题的
1. 标记阶段。将所有的对象看成图的节点,根据对象的引用关系构造图结构。从图的根节点遍历所有的对象,所有访问到的对象被打上标记,表明对象是“可达”的。
2. 清除阶段。遍历所有对象,如果发现某个对象没有标记为“可达”,则就回收。
3. 分代回收
回收垃圾时,程序是需要暂停的,故为了减少程序的暂停时间,采用分代回收
(Generational Collection
)降低垃圾收集耗时。
分代回收基于这样的法则:
1. 大部分的对象生命周期短,大部分对象都是朝生夕灭。
2. 经历越多次数的垃圾收集且活下来的对象,说明该对象越不可能是垃圾,应该越少去收集
分为3代,第0代(G0),第1代(G1),第2代(G2),本质上是一种链表
对象刚创建时,将其放入G0
经过一次扫描,若其引用计数不为0,就将其放进G1,G1的扫描频率降低
再经过一次扫描,若G1中的对象引用计数不为0,就将其放进G2,G2的扫描频率更低
主要是由于在多次扫描下仍存活就代表着该对象使用频繁,生命周期长,就没必要频繁的扫描了
4. 触发垃圾回收的情况
1. 当gc模块的计数器达到阈值的时候,自动回收垃圾
2. 调用gc.collect(),手动回收垃圾
3. 程序退出的时候,python解释器来回收垃圾
5. 触发GC扫描的时机
当某代中分配的对象数量与被释放的对象之差达到某个阈值的时,将触发对该代的扫描。当某代 触发扫描时,比该代年轻的代也会触发扫描。
确定阈值:
import gc
threshold = gc.get_threshold()
print("各代的阈值:", threshold)
# 设置各代阈值
# gc.set_threshold(threshold0[, threshold1[, threshold2]])
gc.set_threshold(800, 20, 20)
#运行结果:各代的阈值: (700, 10, 10)
假设阀值是(700,10,10):
当计数器从(699,3,0)增加到(700,3,0),gc模块就会执行gc.collect(0),即检查一代对象的垃圾,并重置计数器为(0,4,0)
当计数器从(699,9,0)增加到(700,9,0),gc模块就会执行gc.collect(1),即检查一、二代对象的垃圾,并重置计数器为(0,0,1)
当计数器从(699,9,9)增加到(700,9,9),gc模块就会执行gc.collect(2),即检查一、二、三代对象的垃圾,并重置计数器为(0,0,0)