在我所接触到的语言虚拟机中,使用引用计数法进行对象回收的,只有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。


当然,我们这里说的是尽量这么做,并不是说,不这么做就会带来多么严重的问题,只是一种推荐做法。好了,今天就到这里了。