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 代扫描,则进行全对象扫描
工作流程如下:
- 新创建的对象默认被放入第 0 代,当新增对象数量 ≥ 700 时,触发 0 代垃圾回收
- 如果对象在第 0 代回收后仍然存活(即没被回收掉),就会被晋升到第 1 代
- 当第 0 代对象进行 10 回收后,触发 1 次 1 代回收
- 当第 1 代中的对象经过回收后仍存活,会晋升到第2代
- 当第 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())