1. CPython介绍
Python 是一种解释型编程语言。你的 Python 代码实际上被编译成更多计算机可读的指令,称为字节码 (即我们经常在 __
pycache__
文件夹中见到的 .pyc 文件)。当您运行代码时,这些指令由虚拟机解释。
默认的 Python 实际上是CPython, 是用 C 编写的,运行时由它解释 Python 字节码。除 CPython 之外还有其他实现
名称 | CPython | IronPython | Jython | PyPy |
语言 | C | C# | Java | Python |
本文我只介绍由 Python 的默认实现 CPython 完成的内存管理. 在Python中, 一切都是对象, (甚至 int, str 这些数据类型也是对象), 但是C语言本身不支持 OOP (面向对象编程), CPython的实现中, 有一个 struct 叫做 PyObject,CPython 中的每个其他对象都使用它。
Python中的所有对象都可以理解为是 PyObject 的子类. PyObject只包含2个内容
- ob_refcnt : 引用计数
- ob_type: 指向实际对象类型的指针
ob_type 指向的类型, 是另一种 C struct, 它描述一个Python 对象 (例如 str 或 list). 每个对象都有一个内存分配器和释放器, 用于获取和释放内存.
2. 垃圾收集
前面提到, Python 中的每个对象都有一个引用计数和一个指向类型的指针。当对象的引用计数为 0 时, Python 解释器就会从内存中将该对象删除.
2.1 引用计数的增加与减少
以下3种行为会使得对象的引用增加
- 被分配给一个变量
- 被作为参数传递
- 被包含在其他可变对象中, 如列表和字典
可以使用sys模块中的getrefcount方法检查对象的当前引用计数, 将对象作为参数传递给getrefcount方法会增加引用计数1
In [1]: import sys
In [2]: a = [1, 2] # 列表[1, 2] 赋值给变量a, 引用计数为 1
In [3]: sys.getrefcount(a) # a 作为参数传给函数, 引用计数+1, 为2
Out[3]: 2
In [4]: b = a # 列表[1, 2] 赋值给变量b, 引用计数+1
In [5]: sys.getrefcount(a)
Out[5]: 3
In [6]: dct = {'key': a} # a 包含在字典lst中, 引用计数+1
In [7]: sys.getrefcount(a)
Out[7]: 4
与引用计数增加的行为相反, 删除变量可以使引用计数减少
In [8]: del dct
In [9]: sys.getrefcount(a)
Out[9]: 3
In [10]: del b
In [11]: sys.getrefcount(a)
Out[11]: 2
# getrefcount函数将引用计数+1, 函数执行完毕后 a 的引用计数为 1
In [10]: del a # a的引用计数为0, 内存中释放列表[1, 2]对象
3. 避免写入冲突--GIL
可以把计算机的内存理解为一个笔记本, 不同的进程就是不同的作者, 大家可以共同在笔记本中书写和擦掉自己的内容. 如果不同的作者都在同一页笔记本写数据, 整个页面相互叠加, 双方的内容都变得不可读.
CPython的解决方案是增加一个全局解释器锁 GIL (Global Interpreter Lock), GIL 通过锁定整个解释器使得同一时刻, 只有一个线程可以拥有解释器锁, 避免了内存冲突. 可以把 GIL 理解为一跟公用的笔, 同一时刻只能有一个作者能申请使用笔来写入笔记.
另一个原因是, 上文提到垃圾收集依赖于对象的引用计数. 引用计数变量需要避免被两个线程同时增加或减少其值. 例如如下代码, 此时列表 [1] 的引用计数为2.
b = a = [1]
若两个进程同时执行 del b 和 del a , 2个进程获取的引用计数都是2, 将其 -1 之后, 引用计数还是1. 造成内存泄漏, 对象 [1] 永远停留在内存中, 直到Python解释器结束.
总结
CPython中, 对所有对象维护一个引用计数, 一旦下降到0, 将对象从内存中删除.
CPython依赖全局解释器锁来避免内存冲突.