在我所接触到的语言虚拟机中,使用引用计数法进行对象回收的,只有Python,我这节课就介绍一下Python的垃圾回收机制,以及使用Python进行编程时要注意的一些地方。
前边的课程就介绍过了,我们这个课程虽然聚焦在Java,但遇到和其他语言相通的地方,或者是可以相互借鉴的地方,也会介绍一些其他语言的知识。本节课的内容与Java没有任何关系,如果只对Java语言感兴趣,而对Python等其他语言没什么兴趣的,就可以选择跳过这节课。不学这节课对后面的课程不会造成任何影响。
阅读Python源码
Python的源码比较简单,在这里可以下载:
Python Release Python 2.7.13
把源码包下载下来,解开以后,我们可以看到这些目录:
root@ecs-2f21:~/python/Python-2.7.13# ls -ls
total 1016
12 -rw-r--r-- 1 1000 1000 10914 Dec 18 2016 aclocal.m4
44 -rwxr-xr-x 1 1000 1000 43940 Dec 18 2016 config.guess
36 -rwxr-xr-x 1 1000 1000 36350 Dec 18 2016 config.sub
440 -rwxr-xr-x 1 1000 1000 443361 Dec 18 2016 configure
144 -rw-r--r-- 1 1000 1000 141651 Dec 18 2016 configure.ac
4 drwxr-xr-x 22 1000 1000 4096 Dec 18 2016 Demo
4 drwxr-xr-x 18 1000 1000 4096 Dec 18 2016 Doc
4 drwxr-xr-x 2 1000 1000 4096 Dec 18 2016 Grammar
4 drwxr-xr-x 2 1000 1000 4096 Dec 18 2016 Include
8 -rwxr-xr-x 1 1000 1000 7122 Dec 18 2016 install-sh
12 drwxr-xr-x 47 1000 1000 12288 Dec 18 2016 Lib
16 -rw-r--r-- 1 1000 1000 12767 Dec 18 2016 LICENSE
4 drwxr-xr-x 11 1000 1000 4096 Dec 18 2016 Mac
48 -rw-r--r-- 1 1000 1000 48384 Dec 18 2016 Makefile.pre.in
4 drwxr-xr-x 4 1000 1000 4096 Dec 18 2016 Misc
4 drwxr-xr-x 9 1000 1000 4096 Dec 18 2016 Modules
4 drwxr-xr-x 3 1000 1000 4096 Dec 18 2016 Objects
4 drwxr-xr-x 2 1000 1000 4096 Dec 18 2016 Parser
4 drwxr-xr-x 9 1000 1000 4096 Dec 18 2016 PC
4 drwxr-xr-x 2 1000 1000 4096 Dec 18 2016 PCbuild
36 -rw-r--r-- 1 1000 1000 35082 Dec 18 2016 pyconfig.h.in
4 drwxr-xr-x 2 1000 1000 4096 Dec 18 2016 Python
60 -rw-r--r-- 1 1000 1000 55664 Dec 18 2016 README
4 drwxr-xr-x 5 1000 1000 4096 Dec 18 2016 RISCOS
104 -rw-r--r-- 1 1000 1000 99034 Dec 18 2016 setup.py
4 drwxr-xr-x 23 1000 1000 4096 Dec 18 2016 Tools
这其中,最核心的就只有Python和Objects两个目录而已。Python目录下是虚拟机运行的各种逻辑,Python的解释器主要就是在ceval.c这个文件中。而Objects目录下则是Python对象的定义。
对象的定义
我们来看一下,Python的一个Object到底是啥。
我们以最简单的浮点数为例。在Include/floatobject.h中,python中的浮点数是这么定义的:
typedef struct {
PyObject_HEAD
double ob_fval;
} PyFloatObject;
我们继续查看PyObject_HEAD的定义:
/* PyObject_HEAD defines the initial segment of every PyObject. */
#define PyObject_HEAD \
_PyObject_HEAD_EXTRA \
Py_ssize_t ob_refcnt; \
struct _typeobject *ob_type;
这是一个宏定义,那个EXTRA,在正常编译时,是空的。所以,直接展开所有宏,那么PyFloatObject的定义就是这样子的:
typedef struct {
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
double ob_fval;
} PyFloatObject;
这样就很清楚了,ob_refcnt就是引用计数,而ob_val则是真正的值。例如我写一行这样的代码:
a = 1000.0
a = 2000.0
在执行第一句时,Python虚拟机真正执行的逻辑是创建一个PyFloatObject对象,然后使它的ob_fval为1000.0,同时,它的引用计数为1。
当执行到第二句时,创建一个值为2000.0的PyFloatObject对象,并且使这个对象的引用计数为1,而前一个对象的引用计数就要减1,从而变成0。那么前一个对象就会被回收。
引用计数
在Python中,引用计数的维护是通过这两个宏来实现的:
#define Py_INCREF(op) ( \
_Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA \
((PyObject*)(op))->ob_refcnt++)
#define Py_DECREF(op) \
do { \
if (_Py_DEC_REFTOTAL _Py_REF_DEBUG_COMMA \
--((PyObject*)(op))->ob_refcnt != 0) \
_Py_CHECK_REFCNT(op) \
else \
_Py_Dealloc((PyObject *)(op)); \
} while (0)
这两个宏位于Include/object.h中。这里面的的DEBUG宏大家可以自己去看,不是特别关键,重要的在于ob_refcnt增一和减一的操作。
增一就不说了,没什么特别的,重点是减一,如果减完了以后ob_refcnt为0,就会执行_Py_Dealloc来释放内存空间。这一点与上节课我们介绍过的是引用计数为0的情况是一致的。_Py_Dealloc还是一个宏,而且使用了函数指针,如果对这个问题有兴趣的话,可以继续查看下去,我这里就不再介绍了。
我们再来看一下,调用的地方。
def fun():
global a
a = 1
如果使用dis来反编译,就会看到这样的字节码:
>>> dis.dis(test.fun)
3 0 LOAD_CONST 1 (1)
3 STORE_GLOBAL 0 (a)
6 LOAD_CONST 0 (None)
9 RETURN_VALUE
那么,我们再看一下STORE_GLOBAL的实现:
>>> dis.dis(test.fun)
3 0 LOAD_CONST 1 (1)
3 STORE_GLOBAL 0 (a)
6 LOAD_CONST 0 (None)
9 RETURN_VALUE
TARGET(STORE_GLOBAL)
{
w = GETITEM(names, oparg);
v = POP();
err = PyDict_SetItem(f->f_globals, w, v);
Py_DECREF(v);
if (err == 0) DISPATCH();
break;
}
static int
dict_set_item_by_hash_or_entry(register PyObject *op, PyObject *key,
long hash, PyDictEntry *ep, PyObject *value)
{
register PyDictObject *mp;
register Py_ssize_t n_used;
mp = (PyDictObject *)op;
assert(mp->ma_fill <= mp->ma_mask); /* at least one empty slot */
n_used = mp->ma_used;
Py_INCREF(value);
Py_INCREF(key);
if (ep == NULL) {
if (insertdict(mp, key, hash, value) != 0)
return -1;
}
else {
if (insertdict_by_entry(mp, key, hash, ep, value) != 0)
return -1;
}
在这个函数里,对key, value都执行了增加了一次引用计数。而在insertdict_by_entry中,会对原来的引用减一。更加详细的内容大家可以自己去看,我这里就不介绍了。
当然,使用了引用计数的地方,就会存在循环引用。那么Python肯定会提供其他的方案来解决。本节课就先不介绍了。
这里只提一点,就是在写Python代码的时候,我们应该尽量避免循环引用。要做到这个,就要注意两点,第一,就是在设计的时候,尽量使用单向引用;第二,在对象失效以后,将它对其他对象的引用,都设为null。
当然,我们这里说的是尽量这么做,并不是说,不这么做就会带来多么严重的问题,只是一种推荐做法。好了,今天就到这里了。
在这个函数里,对key, value都执行了增加了一次引用计数。而在insertdict_by_entry中,会对原来的引用减一。更加详细的内容大家可以自己去看,我这里就不介绍了。
当然,使用了引用计数的地方,就会存在循环引用。那么Python肯定会提供其他的方案来解决。本节课就先不介绍了。
这里只提一点,就是在写Python代码的时候,我们应该尽量避免循环引用。要做到这个,就要注意两点,第一,就是在设计的时候,尽量使用单向引用;第二,在对象失效以后,将它对其他对象的引用,都设为null。
当然,我们这里说的是尽量这么做,并不是说,不这么做就会带来多么严重的问题,只是一种推荐做法。好了,今天就到这里了。