楔子
如果对编程语言进行分类的话,一般可以分为静态语言和动态语言,也可以分为编译型语言和解释型语言。但个人觉得还有一种划分标准,就是是否自带垃圾回收。关于有没有垃圾回收,陈儒老师在《Python 2.5源码剖析》中,总结得非常好。
对于像 C 和 C++ 这类语言,程序员被赋予了极大的自由,可以任意地申请内存。但权力的另一面对应着责任,程序员最后不使用的时候,必须负责将申请的内存释放掉,并把无效指针设置为空。可以说,这一点是万恶之源,大量内存泄漏、悬空指针、越界访问的 bug 由此产生。
而现代的开发语言(比如 C#、Java)都带有垃圾回收机制,将开发人员从维护内存分配和清理的繁重工作中解放出来,开发者不用再担心内存泄漏的问题,但同时也被剥夺了和内存亲密接触的机会,并牺牲了一定的程序运行效率。不过好处就是提高了开发效率,并降低了 bug 发生的概率。
由于现在的垃圾回收机制已经非常成熟了,把对性能的影响降到了最低,因此大部分场景选择的都是带垃圾回收的语言。
而 Python 里面同样具有垃圾回收,只不过它是为引用计数机制服务的。所以解释器通过引用计数和垃圾回收,代替程序员进行繁重的内存管理工作,关于垃圾回收我们后面会详细说,先来看一下引用计数。
引用计数
Python 一切皆对象,所有对象都有一个 ob_refcnt 字段,该字段维护着对象的引用计数,从而也决定对象的存在与消亡。下面来探讨一下引用计数,当然引用计数在介绍 PyObject 的时候说的很详细了,这里再回顾一下。
但需要说明的是,比起类型对象,我们更关注实例对象的行为。引用计数也是如此,只有实例对象,我们探讨引用计数才是有意义的。因为内置的类型对象超越了引用计数规则,永远都不会被析构,或者销毁,因为它们在底层是被静态定义好的。同理,自定义的类虽然可以被回收,但是探讨它的引用计数也是没有价值的。我们举个栗子:
class A:
pass
del A
首先 del 关键字只能作用于变量,不可以作用于对象。比如 e = 2.71,可以 del e,但是不可以 del 2.71,这是不符合语法规则的。因为 del 的作用是删除变量,并将其指向的对象的引用计数减 1,所以我们只能 del 变量,不可以 del 对象。
至于 def、class 语句执行完之后拿到的也是变量,前面说了,Python 虽然一切皆对象,但我们拿到的都是对象的泛型指针。比如上面代码中的 class A,它会先创建一个类对象,然后再让变量 A 指向这个类对象。所以我们拿到的 A 也是一个变量,只要是变量,就可以被 del。但是 del 变量只是删除了该变量,换言之就是让该变量无法再被使用,至于变量指向的对象是否会被回收,就看是否还有其它的变量也指向它。
总结:对象是否会被回收,完全由解释器判断它的引用计数是否为 0 所决定。
引用计数的相关操作
操作引用计数无非就是将其加一或减一,至于什么时候加一、什么时候减一,在介绍 PyObject 的时候已经说的很详细了。这里我们通过源码,看看引用计数具体是怎么操作的。
在底层,解释器会通过 Py_INCREF 和 Py_DECREF 两个宏来增加和减少对象的引用计数,而当对象的引用计数为 0 时,会调用对应的析构函数来销毁该对象,这个析构函数由对象的类型对象内部的 tp_dealloc 字段决定。
下面我们来看看底层实现,不过在介绍 Py_INCREF 和 Py_DECREF 之前,先来看几个其它的宏,这些宏非常常见,有必要单独说一下。
// Include/object.h
// 将对象的指针转成 PyObject *
#define _PyObject_CAST(op) ((PyObject*)(op))
// 将对象的指针转成 PyVarObject *
#define _PyVarObject_CAST(op) ((PyVarObject*)(op))
// 返回对象的引用计数,即对象的 ob_refcnt 字段
#define Py_REFCNT(ob) (_PyObject_CAST(ob)->ob_refcnt)
// 返回对象的类型,即对象的 ob_type 字段
#define Py_TYPE(ob) (_PyObject_CAST(ob)->ob_type)
// 返回对象的长度,即对象的 ob_size 字段
#define Py_SIZE(ob) (_PyVarObject_CAST(ob)->ob_size)
然后再来看看 Py_INCREF 和 Py_DECREF,它们负责对引用计数执行加一和减一操作。
// Include/object.h
// 将对象的 ob_refcnt 加 1
#define Py_INCREF(op) _Py_INCREF(_PyObject_CAST(op))
static inline void _Py_INCREF(PyObject *op)
{
_Py_INC_REFTOTAL;
op->ob_refcnt++;
}
// 将对象的 ob_refcnt 减 1
#define Py_DECREF(op) _Py_DECREF(__FILE__, __LINE__, _PyObject_CAST(op))
tatic inline void _Py_DECREF(const char *filename, int lineno,
PyObject *op)
{
(void)filename; /* may be unused, shut up -Wunused-parameter */
(void)lineno; /* may be unused, shut up -Wunused-parameter */
_Py_DEC_REFTOTAL;
// 将引用计数减 1 之后进行判断,如果结果不等于 0,则什么也不做
if (--op->ob_refcnt != 0) {
// 正常情况下,Py_REF_DEBUG 宏不会被定义,因为引用计数不可能小于 0
#ifdef Py_REF_DEBUG
if (op->ob_refcnt < 0) {
_Py_NegativeRefcount(filename, lineno, op);
}
#endif
}
// 否则说明引用计数为 0,意味着对象已经不被任何变量引用了,那么应该被销毁
else {
// 调用 _Py_Dealloc 将对象销毁,这个 _Py_Dealloc 函数内部的逻辑很简单
// 虽然里面存在宏判断,但如果只看编译后的最终结果,那么代码就只有下面两行
/* destructor dealloc = Py_TYPE(op)->tp_dealloc;
* (*dealloc)(op);
*/
// 会获取类型对象的 tp_dealloc,然后调用,销毁实例对象
_Py_Dealloc(op);
}
}
以上就是 Py_INCREF 和 Py_DECREF 两个宏的具体实现,但是它们不能接收空指针,如果希望能接收空指针,那么可以使用另外两个宏。
// Include/object.h
#define Py_XINCREF(op) _Py_XINCREF(_PyObject_CAST(op))
static inline void _Py_XINCREF(PyObject *op)
{
if (op != NULL) {
Py_INCREF(op);
}
}
#define Py_XDECREF(op) _Py_XDECREF(_PyObject_CAST(op))
static inline void _Py_XDECREF(PyObject *op)
{
if (op != NULL) {
Py_DECREF(op);
}
}
所以 Py_XINCREF 和 Py_XDECREF 会额外对指针做一次判断,如果为空则什么也不做,不为空再调用 Py_INCREF 和 Py_DECREF。而当一个对象的引用计数为 0 时,与该对象对应的析构函数就会被调用。
但要特别注意的是,我们上面说调用析构函数之后会回收对象,或者说销毁对象,意思是将这个对象从内存中抹去,但并不意味着要释放空间,也就是对象没了,但对象占用的内存却还在。
如果对象没了,占用的内存也要释放的话,那么频繁申请、释放内存空间会使 Python 的执行效率大打折扣,更何况 Python 已经背负了人们对其执行效率的不满这么多年。
所以 Python 底层大量采用了缓存池的技术,使用这种技术可以避免频繁地申请和释放内存空间。因此在析构的时候,只是将对象占用的空间放到缓存池中,并没有真的释放。
这一点,在后面剖析内置实例对象的实现中,将会看得一清二楚,因为大部分内置的实例对象都会有自己的缓存池。
小结
到此我们就把这些基础概念说完了,后续你会发现目前花费的这些笔墨都是值得的,总之先对 Python 有一个宏观的认识,然后再学习具体的数据结构就简单多了。
所以从下一篇文章开始就要详细剖析内置对象的底层实现了,比如浮点数、复数、整数、布尔值、None、bytes 对象、bytearray 对象、字符串、元组、列表、字典、集合等等,所有的内置对象都会详细地剖析一遍,看看它是如何实现的。
有了目前为止的这些基础,我们后面就会轻松很多。先把对象、变量等概念梳理清楚,然后再来搞这些数据结构的底层实现。
欢迎大家关注我的公众号:古明地觉的编程教室。
如果觉得文章对你有所帮助,也可以请作者吃个馒头,Thanks♪(・ω・)ノ。