什么是垃圾?——Python内存管理机制

2023年 7月 14日 44.9k 0

什么是垃圾?

当我们的Python解释器在执行到定义变量的语法时,会申请内存空间来存放变量的值,而内存的容量是有限的,这就涉及到变量值所占用内存空间的回收问题。那么什么是垃圾呢?简单来说垃圾就是指:当一个对象或者说变量没有用了,这时候它就是垃圾了。

a = 10000 # 开辟内存存储值
a = 30000 # 开辟新内存存储值
# 此时10000没有被引用了,变成垃圾,需要被回收内存。

内存泄漏与内存溢出

上面的代码看到有”垃圾“产生了,如果不处理咋办?举个例子:

  • 你厕所拉屎不冲水,越拉越多,然后有一天,你又想拉,发现厕所堵了,没地拉屎
  • 你觉得房间还空旷,继续拉屎,慢慢地你房间满了,你也就被屎山覆盖完蛋了。
  • 上面的结果换句话说就是下面:

    内存泄漏:程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光 内存溢出:程序在申请内存时,没有足够的内存空间供其使用,出现 out of memory

    数据池和缓存:

  • 小整数池:Python会自动缓存范围在[-5, 256]内的整数对象,避免频繁创建和销毁对象。当多个变量引用相同的整数对象时,它们指向同一个内存地址的同一个对象。
  • intern机制:intern机制用于缓存并重复使用一些字符串对象,以减少内存开销和提高性能。当Python解释器遇到字符串字面量时,会先检查该字符串是否存在于intern字符串池中。如果存在,则直接返回对应字符串对象的引用;否则,创建一个新的字符串对象,并将其添加到intern字符串池中,以便后续重复使用。
  • 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的引用计数机制是内存管理的基础。当对象被创建时,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嘛?你怎么老想着性能呢?

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论