深入Python中引用计数

深入Python中引用计数

在python中的垃圾回收机制主要是以引用计数为主要手段以标记清除和隔代回收机制为辅的手段 。可以对内存中无效数据的自动管理!在这篇文章,带着这个问题来一直往下看:怎么知道一个对象能不能被调用了呢?

回顾内存地址

Python中的任何变量都有对应的内存引用,也就是内存地址。

如果不是容器类型,那么直接引用和赋值,内存地址都是不会的。

>>> a = 1 
>>> b = 1 
>>> id(a) 
140709385600544 
>>> id(b) 
140709385600544 

如果在内存中创建了一个list对象(容器),而且对该对象进行了引用。那么b = [1,2]和c = a有什么区别?

>>> a = [1,2] 
>>> b = [1,2] 
>>> id(a) 
1966828025736 
>>> id(b) 
1966828044488 
>>> c = a 
>>> id(c) 
1966828025736 

首先在内存1966828025736处创建了一个列表 [1,2],然后定义了一个名为a的变量。b = [1,2]会新开一个内存地址,c = a直接赋值直接引用[1,2]的内存地址。

引用计数

在一些代码中,如果存在一些变量但是没有用,会造成内存空间,因此叫做垃圾,所以要回收。

引用计数也是一种最直观,最简单的垃圾收集技术。原理非常简单,每一个对象都包含了两个头部信息,一个是类型标志符,标识这个对象的类型;另一个是计数器,记录当前指向该对象的引用数目,表示这个对象被多少个变量名所引用。

CPython 使用引用计数来管理内存,所有 Python 脚本中创建的实例,都会有一个引用计数,来记录有多少个指针指向它。当引用计数只有 0 时,则会自动释放内存。

在Python中通过sys.getrefcount查看引用计数的方法,

print(sys.getrefcount()) 

注意调用getrefcount()函数会临时增加一次引用计数,得到的结果比预期的多一次。

比如,下面这个例子中,a 的引用计数是 3,因为有 a、b 和作为参数传递的 getrefcount 这三个地方,都引用了一个空列表。

>>> import sys 
>>> a = [] 
>>> b = a 
>>> print(sys.getrefcount(a)) 
3 

我们通过一些例子来看下,可以使python对象的引用计数增加或减少的场景。

import sys 
a = [] 
# 两次引用,一次来自 a,一次来自 getrefcount 
print(sys.getrefcount(a)) 
def func(a): 
    # 四次引用,a,python 的函数调用栈,函数参数,和 getrefcount 
    print(sys.getrefcount(a)) 
 
func(a) 
# 两次引用,一次来自 a,一次来自 getrefcount,函数 func 调用已经不存在 
print(sys.getrefcount(a)) 
 
########## 输出 ########## 
2 
4 
2 

引用计数是用来记录对象被引用的次数,每当对象被创建或者被引用时将该对象的引用次数加一,当对象的引用被销毁时该对象的引用次数减一,当对象的引用次数减到零时说明程序中已经没有任何对象持有该对象的引用,换言之就是在以后的程序运行中不会再次使用到该对象了,那么其所占用的空间也就可以被释放了了。

计数增加和减少

下面引用计数增加的场景:

  • 对象被创建并赋值给某个变量,比如:a = 'ABC'
  • 变量间的相互引用(相当于变量指向了同一个对象),比如:b=a
  • 变量作为参数传到函数中。比如:ref_method(a),
  • 将对象放到某个容器对象中(列表、元组、字典)。比如:c = [1, a, 'abc']

引用计数减少的场景:

  • 当一个变量离开了作用域,比如:函数执行完成时,执行方法前后的引用计数保持不变,这就是因为方法执行完后,对象的引用计数也会减少,如果在方法内打印,则能看到引用计数增加的效果。
  • 对象的引用变量被销毁时,比如del a或者del b。注意如果del a,再去获取a的引用计数会直接报错。
  • 对象被从容器对象中移除,比如:c.remove(a)
  • 直接将整个容器销毁,比如:del c
  • 对象的引用被赋值给其他对象,相当于变量不指向之前的对象,而是指向了一个新的对象,这种情况,引用计数肯定会发生改变。(排除两个对象默认引用计一致的场景)。