python的全局解释器锁(GIL)到垃圾回收机制

python全局解释器锁产生的背景

python添加全局解释器锁的原因是什么?

垃圾清理机制的引用计数法导致的

Python的垃圾清理(GC)

清理垃圾首先要找到垃圾,然后进行清理。

在Python中所谓垃圾对象是指

在程序中不再被任何变量引用的对象

这些对象已经"失去了使用价值",程序再也无法访问它们,因此可以被安全地销毁,从而回收其所占的内存资源。(不在被程序使用的代码)

引用计数

正常引用

为了能够跟踪对象被引用的次数,在 Python 中,每个对象结构都维护一个引用计数字段,用于记录该对象被引用的次数。我们可以通过 sys.getrefcount 函数来获得某个对象被引用的次数。当对象的引用计数为 0 时,该对象就会被自动回收。

python 复制代码
import sys


class Demo():
    def __del__(self):
        print('对象销毁')


def test():
    a = Demo()
    # 注意:此处输出 2,新创建的 Demo 对象初始引用计数为 1(由变量 a 引用),用 sys.getrefcount(a) 时,函数参数会临时增加一次引用,因此输出 2。
    print('Demo 对象引用计数:', sys.getrefcount(a))
    b = a
    # 此处 b 又引用对象,会导致 Demo 对象引用计数增加 1
    print('Demo 对象引用计数:', sys.getrefcount(a))
    # 此处断开了 a 和 Demo 对象之间的引用,会导致引用计数减 1
    del a
    print('Demo 对象引用计数:', sys.getrefcount(b))
    # 当 Demo 对象的引用计数为 0 时,该对象就会被自动释放,此时会自动调用对象的 __del__ 函数
    # 例如:当 test 程序执行结束,Demo 对象的最后一个引用 b 被销毁,此时引用计数为 0
    print('test 函数执行结束')


if __name__ == '__main__':
    test()
    input("拦截掉程序"*20)

打印输出

循环被引用

引用计数自身也存在一个致命问,即:循环引用,它会导致某些不再使用的对象引用计数无法为 0,导致对象无法正确释放。(导致内存泄露) 例如:

python 复制代码
import sys

class Demo:
    def __init__(self):
        self.ref = None

    def __del__(self):
        print('对象销毁')


def test():
    a = Demo()
    b = Demo()

    # 循环引用发生处
    a.ref = b
    b.ref = a

    print('a 的引用计数:', sys.getrefcount(a))
    print('b 的引用计数:', sys.getrefcount(b))

    # 当程序结束,a 和 b 离开作用域后,对象仍互相引用,虽然没有外部变量引用它们,但因为彼此引用,它们的引用计数都为 1,因此无法被引用计数机制回收。
    # 即:函数结束后,会导致循环引用出现
    print('test 函数执行结束')


if __name__ == '__main__':
    test()
    # 暂停程序,观察 test 函数中是否正确清理对象
    input("拦截掉程序"*20)

打印输出

标记清除

目的:为了解决引用计数器循环引用的不足。

实现:在python的底层再维护一个链表,链表中专门放那些可能存在循环引用的对象(list/tuple/dict/set)

python 复制代码
import sys
import time
class Demo:
    def __init__(self):
        self.ref = None
    def __del__(self):
        print('对象销毁')
def test():
    a = Demo()
    b = Demo()
    a.ref = b
    b.ref = a
    print('a 的引用计数:', sys.getrefcount(a))
    print('b 的引用计数:', sys.getrefcount(b))
    print('test 函数执行结束')
if __name__ == '__main__':
    test()
    c = Demo()
    c.ref = Demo()
    # 假设在此时机,Python 从根对象开始扫描所有对象
    # 发现:
    # 1. 在 main 中创建的两个 Demo 对象可以从根对象跟踪到,标记为可达(保留)
    # 2. 在 test 函数中的两个 Demo 对象无法从根对象扫描到,标记为不可达对象(清除)
    # 那么,如何启动标记清除? 通过 gc.collect() 函数即可
    # 注意:该函数一般不用手动调用,GC 会在合适的时机自动调用
    time.sleep(10)
    import gc
    gc.collect()
    # 暂停程序,观察 test 函数中是否正确清理对象
    input()

在Python内部某种情况下触发,回去扫描可能存在循环应用的链表中的每个元素,检查是否有循环引用,如果有则让双方的引用计数器-1;如果是0则垃圾回收

分代回收机制

  • 第 0 代(新生代):高频率扫描的对象
  • 第 1 代(中生代):中频率扫描的对象
  • 第 2 代(老年代):低频率扫描的对象

注意:

  • 分代回收时通常只处理特定的一代,从而避免频繁扫描整个内存,提高 GC 效率
  • 如果触发 1 代对象扫描,则会对 0 代和 1 代进行扫描;如果触发 2 代扫描,则进行全对象扫描

工作流程如下:

  1. 新创建的对象默认被放入第 0 代,当新增对象数量 ≥ 700 时,触发 0 代垃圾回收
  2. 如果对象在第 0 代回收后仍然存活(即没被回收掉),就会被晋升到第 1 代
  3. 当第 0 代对象进行 10 回收后,触发 1 次 1 代回收
  4. 当第 1 代中的对象经过回收后仍存活,会晋升到第2代
  5. 当第 1 代对象进行 10 回收后,触发 1 次 2 代回收
python 复制代码
import gc


if __name__ == '__main__':
    # (700, 10, 10)
    # 第 0 代阈值(700):当 新增对象数量 ≥ 700 时,触发 0 代垃圾回收。
    # 第 1 代阈值(10):当 0 代回收发生 10 次 后,触发一次 1 代回收。
    # 第 2 代阈值(10):当 1 代回收发生 10 次 后,触发一次 2 代回收。
    print(gc.get_threshold())
    # 修改 GC 触发的阈值
    gc.set_threshold(1000, 15, 5)
    print(gc.get_threshold())
相关推荐
Co_Hui2 小时前
JVM 内存结构
jvm
Little Tomato2 小时前
深入浅出高并发:从 JVM 锁竞争到分布式事务的性能博弈
jvm·分布式
南境十里·墨染春水2 小时前
线程池学习(二)线程池理解
java·jvm·学习
小杍随笔3 小时前
【iNovel 后端架构深度解析:基于 Rust + Tauri 2 的桌面应用服务端设计】
jvm·架构·rust
m0_702036534 小时前
CSS如何兼容新旧方案结合响应式容器查询
jvm·数据库·python
LucaJu5 小时前
一次 OOM 线上排查实录
java·jvm·oom·内存溢出
大大杰哥5 小时前
温故知新:Java 线程创建方式的演进与总结
java·开发语言·jvm
网络工程小王6 小时前
【LangGraph 状态持久化(Checkpoint)详解】学习笔记
jvm·人工智能·笔记·langchain
Devin~Y6 小时前
电商AIGC智能客服面试:JVM调优、Spring Cloud微服务、Redis缓存、Kafka消息、K8s观测与RAG落地
java·jvm·spring boot·redis·spring cloud·kafka·kubernetes