什么是垃圾?
当我们的Python解释器在执行到定义变量的语法时,会申请内存空间来存放变量的值,而内存的容量是有限的,这就涉及到变量值所占用内存空间的回收问题。那么什么是垃圾呢?简单来说垃圾就是指:当一个对象或者说变量没有用了,这时候它就是垃圾了。
a = 10000 # 开辟内存存储值
a = 30000 # 开辟新内存存储值
# 此时10000没有被引用了,变成垃圾,需要被回收内存。
内存泄漏与内存溢出
上面的代码看到有”垃圾“产生了,如果不处理咋办?举个例子:
上面的结果换句话说就是下面:
内存泄漏:程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光 内存溢出:程序在申请内存时,没有足够的内存空间供其使用,出现 out of memory
数据池和缓存:
import sys
# 创建两个相同值的字符串对象
str1 = "hello"
str2 = "hello"
# 检查两个字符串对象的内存地址
print(id(str1),id(str2))
# 使用 intern 机制将字符串添加到 intern 字符串池中
str1 = sys.intern(str1)
str2 = sys.intern(str2)
# 再次检查两个字符串对象的内存地址
print(id(str1),id(str2))
这些数据池和缓存机制可以在一定程度上优化Python的内存使用,特别是对于频繁使用的对象类型或值。
垃圾回收
Python的垃圾回收机制用于解决循环引用和内存泄漏的问题。当一个对象不再被引用时,其占用的内存应该被回收以供其他对象使用。python采用的是引用计数机制为主,标记-清除和分代收集(隔代回收)两种机制为辅的策略
一、引用计数
Python的引用计数机制是内存管理的基础。当对象被创建时,Python会分配内存地址,并对对象进行初始化。所有对象都会维护在一个双向循环链表中,该链表称为refchain。每个对象都保存以下信息:
- 链表中指向前后对象的指针
- 对象的类型
- 对象的值
- 对象的引用计数
- 对象的长度(适用于list、dict等可变对象)
引用计数增加的情况包括:对象被创建、对象被其他变量引用、对象作为容器的元素、对象作为参数传递到函数中。 引用计数减少的情况包括:对象的别名被显式销毁、对象的别名被赋值给其他对象、对象从容器中被移除或容器被销毁、引用离开其作用域。
我们可以使用sys模块中的getrefcount()
函数来查看对象的引用计数。需要注意的是,getrefcount()
函数会在内部临时增加对象的引用计数,所以需要减去函数本身的引用计数才能得到对象的真实引用计数。
import sys
# 引用计数
numbers = [1, 2, 3]
ref_count = sys.getrefcount(numbers) - 1
print(ref_count) # 输出 1
# 循环引用:对象互相引用
a = [1, 2]
b = [2, 3]
a.append(b) # 计数2
b.append(a) # 计数2
del a # 计数1
del b # 计数1
# 引用自身
c = [1, 2, 3, 4]
c.append(c)
当一个对象的引用计数为0时,表示没有任何变量引用该对象,可以被回收。但引用计数机制无法解决循环引用的问题。 引用计数的缺点:1. 循环引用 2. 维护引用计数消耗资源
二、标记-清除策略
标记-清除算法:用于解决无法通过引用计数回收的垃圾对象的算法。它的基本思想是从根对象开始,通过标记所有可以访问到的对象,然后清除未标记的对象。清除阶段会释放未被引用的对象占用的内存。 该策略在回收垃圾的时候有两步:
标记阶段: 在此阶段,垃圾回收器会从一组根对象开始,例如全局变量、活动的函数调用栈和特殊的内部数据结构。这些根对象是我们确定的起始点。垃圾回收器会递归地遍历这些根对象,并标记所有可以从根对象访问到的对象。它会使用一种标记机制,通常是在对象的内存布局中添加一个额外的标记位来表示对象的状态。初始状态下,所有对象的标记位都是未标记的。通过遍历对象之间的引用关系,从根对象开始,垃圾回收器会标记所有可达的对象。如果一个对象被标记,则意味着它是活动的,即仍然被程序使用。未被标记的对象则被认为是垃圾对象。
清除阶段: 垃圾回收器会扫描整个堆内存,并清除所有未被标记的可达对象。这些未被标记的可达对象被认为是垃圾,因为它们不再被程序使用。
标记-清除是一种周期策略,每隔一段时间进行一次扫描,这种过程会暂停整个应用程序,等待标记-清除结束后才会恢复应用的运行。
分代回收策略
因为标记-清除策略会让程序阻塞,为了提高垃圾回收的效率,在标记-清除的基础上进一步建立分代回收,是一种空间换时间提高回收效率的策略。
Python将内存划分为不同的代(generation),包括年轻代(第0代)、中年代(第1代)和老年代(第2代)。这样划分的目的是根据对象的存活时间来优化垃圾回收的效率。
通常情况下,年轻代的对象存活时间较短,而老年代的对象存活时间较长。因此,Python的分代回收策略认为,存活时间较长的对象越来越不可能是垃圾,所以在回收时应该更少地去检查老年代的对象。
触发分代回收的条件由阈值控制。
import gc
print(gc.get_threshold()) # 默认策略阈值:(700,10,10)
# 阈值设置为(700, 10, 10),表示当分配对象的个数达到700时,
# 进行一次0代回收;经过10次0代回收后,触发一次1代回收;
# 经过10次1代回收后,触发一次2代回收。
gc.set_threshold(500, 5, 5) # 自己设置回收策略阈值
将对象分为不同的代(Generation),每个代有不同的回收频率。新创建的对象属于第0代(扫描算法),经过一次垃圾回收仍存活的对象会晋升到下一代,而不活跃的对象会被更频繁地回收。
思考
- 什么时候释放不再使用的对象? 可以通过显式地将对象设置为
None
或使用del
关键字来解除对象的引用。 - 如何避免循环引用? 避免循环引用的方法包括使用弱引用或重新设计数据结构以消除循环引用。
- 如何减少内存占用? 对于需要频繁创建和销毁的对象,可以考虑使用缓存, 避免重复创建相同的对象。
当然,咱们搞垃圾系统的,知道那么多这种干啥呢?用python不就是想着easy嘛?你怎么老想着性能呢?