Python的对象引用和垃圾回收

前言

Python 作为一门高级编程语言,以其简洁易用的语法和强大的功能深受开发者喜爱。在使用 Python 进行开发时,理解其内存管理机制,尤其是对象引用和垃圾回收,对于编写高效且内存友好的代码至关重要。接下来我们简单探讨一下Python的对象引用和垃圾回收。

对象引用

在 Python 中,所有数据都是对象,变量是这些对象的引用。当我们进行赋值操作时,实际上是将变量指向对象。例如

a = [1, 2, 3]
b = a

在这段代码中,列表 [1, 2, 3] 是一个对象,a 是对该对象的引用。当我们执行 b = a 时,b 也成为该对象的引用。因此,此时 a 和 b 都指向同一个对象。你可以使用id()函数来查看一下a和b,两个的值应该是一样的。

引用计数

Python 使用引用计数来管理对象的生命周期。每个对象都有一个引用计数器,记录了有多少个引用指向它。当对象的引用计数为零时,说明没有任何引用指向该对象,Python 的垃圾回收机制会释放该对象的内存。

可以使用 sys.getrefcount(obj) 查看某个对象的引用计数。需要注意的是,调用 sys.getrefcount(obj) 本身会增加一次引用计数,因此结果会比实际值多1。

import sys

a = [1,2,3]
b = None
c = [1,2,3]
d = 2
e = 23456
print('a的引用计数: ',sys.getrefcount(a) - 1)
print('b的引用计数: ',sys.getrefcount(b) - 1)
print('c的引用计数: ',sys.getrefcount(c) - 1)
print('d的引用计数: ',sys.getrefcount(d) - 1)
print('e的引用计数: ',sys.getrefcount(e) - 1)

输出结果为:

a的引用计数:  1
b的引用计数:  14342
c的引用计数:  1
d的引用计数:  853
e的引用计数:  2

a和c的引用计数为1,但为什么c为14342呢?因为None是Python中的特殊常量,在很多地方被频繁使用,所以它的引用计数非常高。你可以尝试一下以下代码

a = None
b = None
print(id(a))
print(id(b))
print(None)

你会发现打印的值是一样的。
在上面的引用计数结果中,有一些和预期不符合的现象。

  • d的引用计数,看起来不太正常,我推测变量1应该也和None一样,也在Python中被频繁使用,我查到资料上说**“Python对小整数(通常是 -5 到 256 之间的整数)进行了内部优化,将它们预先分配并缓存,以提高性能和节省内存。”**,
  • 如果你使用了conda的Python环境,对数字对象的引用计数可能是个很奇怪的值,我测试**"d=1"的引用计数是1000000853**,我暂时没找到合理的解释,希望知道的大佬能告知一下。
  • 然后就是"e=23456"的引用计数为2,我查阅资料有说**“Python 解释器可能对大整数对象有其他的内部引用,例如运行时优化或者缓存机制。”**,这个说话也有待商榷,大家可以交流一下。

弱引用

在Python中,弱引用(weak reference)是一种特殊的引用类型,它不会增加其指向对象的引用计数。这意味着,如果只有弱引用指向某个对象,那么这个对象可能会被垃圾回收器回收,即使存在弱引用也不会阻止对象被释放内存。

Python的weakref模块提供了对弱引用的支持。你可以使用weakref模块中的ref类来创建一个弱引用对象。例如:

import weakref
import sys

class RefTest:
    def __init__(self,value):
        self.value = value
        
        
if __name__ == '__main__':
    obj1 = RefTest(10)
    obj2 = obj1
    w_obj1 = weakref.ref(obj1)
    print("obj1的引用计数:",sys.getrefcount(obj1) - 1)
    print("obj2的引用计数:",sys.getrefcount(obj2) - 1)
    print("w_obj1的引用计数:",sys.getrefcount(w_obj1()) - 1)
    del obj1
    print("del obj1后obj2的引用计数:",sys.getrefcount(obj2) - 1)
    print("del obj1后w_obj1的引用计数:",sys.getrefcount(w_obj1()) - 1)
    del obj2
    print("del obj2后w_obj1的引用计数:",sys.getrefcount(w_obj1()) - 1)
    print(w_obj1())

输出结果:

obj1的引用计数: 2
obj2的引用计数: 2
w_obj1的引用计数: 2
del obj1后obj2的引用计数: 1
del obj1后w_obj1的引用计数: 1
del obj2后w_obj1的引用计数: 15107
None

上面可以看到,弱引用对象w_obj1并没有增加引用计数,当del obj1和obj2后,w_obj1就变成了None。(注:del不是删除对象,而是减少对象的引用计数
弱引用通常用于缓存、回调函数和循环引用的解决方案等场景,它们能够避免循环引用导致的内存泄漏问题。weakref模块还有一些其他的方法,请自行测试。

垃圾回收

尽管引用计数机制能够高效地管理大多数对象的内存,但它无法处理循环引用的问题。循环引用指的是两个或多个对象相互引用,形成一个环,而这个环不再被其他对象引用。例如:

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None

node1 = Node(1)
node2 = Node(2)
node1.next = node2
node2.next = node1

在上述代码中,node1 和 node2 互相引用,形成一个循环引用。如果没有额外的机制,这部分内存将永远不会被回收。

为了解决这个问题,Python 引入了基于分代回收的垃圾收集器。垃圾收集器会定期运行,检查并回收不可达的对象

分代回收

  • Python 使用分代垃圾回收机制,将对象分为不同的“代”(generation),根据对象的存活时间和使用情况进行分级管理。

  • 分代管理:新创建的对象属于第0代,经过多次垃圾回收仍然存活的对象会被提升到更高的代,以减少不必要的检查和清理。
    调整垃圾回收参数:可以使用 gc.set_threshold() 调整垃圾回收的参数,控制不同代的垃圾回收频率。

gc模块

Python 提供了 gc 模块,允许开发者手动控制垃圾回收。我们可以使用gc.collect()手动触发垃圾回收,使用 gc.get_objects() 用于获取当前 Python 解释器中所有被垃圾回收器跟踪的对象,获取当前所有的对象列表。

import gc

# 手动触发垃圾回收
gc.collect()

# 查看当前所有对象
print(gc.get_objects())

如果运行上面的代码,会打印出一大串字符串,这应该就是上面这个程序启动后被垃圾回收器跟踪的对象。
通过理解和使用gc模块,我们可以更好地调试内存问题,优化程序性能。但是循环引用的对象,内存是没法进行回收的,下面来看看循环引用的代码

代码示例

我们就以上面那个循环引用为示例,来看一下对象内存占用。

import sys
import gc
import time

# 循环引用示例

node1 = [1]
node2 = [2]
node1.append(node2)
node2.append(node1)
node3 = [34]
id1 = id(node1)
id2 = id(node2)
id3 = id(node3)

objs = gc.get_objects()
print('没有处理对象之前')
for obj in objs:
   if id(obj) == id1 or id(obj) == id2 or id(obj) == id3:
       print('obj =', obj)
del node1
del node2
del node3
time.sleep(10)
#time.sleep(100)
print('del处理后')
for obj in objs:
   if id(obj) == id1 or id(obj) == id2 or id(obj) == id3:
       print('obj =', obj)

    
unreachable_count = gc.collect()
#5time.sleep(100)
print('gc处理后')
for obj in objs:
   if id(obj) == id1 or id(obj) == id2 or id(obj) == id3:
       print('obj=', obj)

输出结果:

有处理对象之前
obj = [1, [2, [...]]]
obj = [2, [1, [...]]]
obj = [34]
del处理后
obj = [1, [2, [...]]]
obj = [2, [1, [...]]]
obj = [34]
gc处理后
obj= [1, [2, [...]]]
obj= [2, [1, [...]]]
obj= [34]

我的预想结果是在del之后,或者在手动gc之后,node3应该不会有内存占用,但结果还是出乎意料,查阅资料,有以下说法:

垃圾回收器执行时机:Python 的垃圾回收器不是在对象不再被引用的时候立即执行,而是根据一定的策略和条件周期性执行。因此,即使手动调用了 gc.collect(),并不一定会立即清理所有的循环引用对象。

这个可以讨论一下。

总结

理解 Python 的对象引用和垃圾回收机制是编写高效 Python 代码的基础。引用计数垃圾回收共同构成了 Python 的内存管理体系,确保程序能够高效运行并合理利用内存。通过善用这些机制,我们可以避免内存泄漏,优化程序性能,提升代码质量。本文中其实也有一些个人的疑点,希望大家一起交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值