python 类中内存不回收问题_面试中的高频问题,如何理解Python内存管理中的垃圾回收机制...

本文详细探讨了Python内存管理中的变量与对象、引用计数、垃圾回收及循环引用问题。介绍了Python如何通过引用计数跟踪对象,以及在循环引用导致内存泄漏时,如何利用标记-清除和分代回收机制来解决问题。最后讨论了垃圾回收的触发时机,包括自动回收和手动回收。
摘要由CSDN通过智能技术生成
e614a67a04daf7f3a22d1f69553a0731.png

变量与对象

Python 作为一种动态类型的语言,其对象和引用分离。在 Python 中万物皆对象,因此Python 的存储问题等同于对象的存储问题,对于每个对象,Python 都会分配一块内存空间去存储它 。

我们通过一个简单的赋值语句来理解 变量与对象,如下:

232e5f8caf91837d9cb69d114e9c7d12.png

变量(testops),通过变量指针引用对象,变量指针指向具体对象的内存空间,取对象的值。对象 (9527),类型已知,每个对象都包含一个头部信息(类型标识符和引用计数器)。Python中变量名没有类型,类型属于对象,对象的类型决定了变量的类型。

d4bc34410692af9ae248c7a3898de488.png

如上,整数 9527 为一个对象, test 是一个变量。利用赋值语句,引用 test 指向对象 9527 。9527 对象存储在内存中,我们可以通过 id 函数,查看对象的内存地址,引用示意图如下:

649894f49e2a2be42194a0427bfa77c3.png

对于整数和短小的字符等,会触发Python的缓存机制,即Python将这些对象进行缓存,不会为相同的对象分配不同的内存空间,如下:

0f5b9e4fef68f224a6349f8751f16a07.png

如上,我们使用 is 关键字判断两个引用所指的对象是否相同。可以看到,由于Python缓存了小整数(其实也缓存了短字符串,Python2),因此每个对象只存有一份,比如,使用赋值语句创建小整数,如 27,并没有创建出新的对象,而是创建了一个引用。而当使用赋值语句创建大的整数可以有多个相同的对象,如使用赋值语句创建大整数 27000,此时创建出多个对象。


引用计数

048da8eb503ad8299524dc0880386a3a.png

在Python中,每个对象都有指向该对象的引用总数,即引用计数(reference count)。一个对象会记录着引用自己的对象的个数,每增加1个引用,个数加1,每减少1个引用,个数减1。在垃圾回收过程中,利用引用计数器方法,在检测到对象引用个数为 0 时,对普通的对象进行释放内存的机制。

我们可以使用 sys.getrefcount 方法,来查看每个对象的引用计数。需要注意的是,当使用某个引用作为参数,传递给 getrefcount方法时,参数实际上创建了一个临时的引用。因此,getrefcount 方法所得到的结果比期望的多1。

2890ebc7dedce0c555d0a5fca8bfb105.png

由上可见,l 中的 [t,27] 两个元素,都指向了同一个对象,实际上,容器对象(如,列表、字典等)中包含的并不是元素对象本身,是指向各个元素对象的引用。同时,l 的引用计数随着 ll 的创建和删除,引用计数也随着增加1和减少1。


导致引用计数增加的场景如下:

  • 对象被创建:t = 27
  • 其它的别名被创建:ll = l
  • 作为参数传递给函数:getrefcount(l)
  • 作为容器对象的一个元素:l = [t, 27]

导致引用计数减少的场景如下:

  • 对象的别名被显式的销毁:del ll
  • 对象的一个别名被赋值给其他对象:l = 789
  • 对象所在的容器被销毁或从容器中删除对象 如,del ll l.remove(t)
  • 一个本地引用离开了它的作用域,比如上面的 getrefcount(x) 函数结束时,x指向的对象引用减1。

引用计数中的循环引用

a938a8d1f1dca159e7704717d033526e.png

循环引用即对象之间进行相互引用,出现循环引用后,利用上述引用计数机制无法对循环引用中的对象进行释放空间,从而导致内存泄漏,这就是循环引用问题,如下:

89ff08ad592d7b9682b8d24593b3bbde.png

对象 test 中的元素引用 ops,而对象 ops 中元素同时来引用 test ,从而造成仅仅删除 testops对象,无法释放其内存空间,因为他们依然在被引用(引用个数不为0)。进一步解释就是循环引用后,test ops 被引用个数为2,删除 test ops 对象后,两者被引用的个数变为1,并不是0,而Python只有在检查到一个对象的被引用个数为0时,才会自动释放其内存,所以这里无法释放 test ops 的内存空间,因此这也是导致内存泄漏的原因之一。


垃圾回收

20b2c2d2809f97d0375a13d3aec33db0.png

在Python中的对象越来越多,占用的内存越来越大,垃圾回收机制就是将没用的对象清除,释放内存。Python垃圾回收采用引用计数机制为主,标记-清除和分代回收机制为辅的策略,其中标记清除机制用来解决技术引用带来的循环引用而无法释放内存的问题,分代回收机制是为提升垃圾回收的效率。

当Python的对象的引用计数降为 0时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾。比如新建一个对象,被赋值给某个变量,则该对象的引用计数变为1。如果变量被删除,对象的引用计数为0,那么该对象就会被垃圾回收。

041351d0d831b125aaeb17d81975208b.png

如上,执行 del t 后,已经没有任何引用指向之前建立的对象 9527,该对象引用计数变为0,用户不可能通过任何方式使用这个对象,当垃圾回收启动时,Python扫描到这个引用计数为0的对象,就将它所占用的内存进行回收。


标记-清除机制——解决循环引用问题

e0639afc8c215572dcf53eab2a1a1665.png

标记-清除机制顾名思义,首先标记对象(垃圾检测),然后清除垃圾(垃圾回收),标记清除用来解决引用计数机制产生的循环引用,进而导致内存泄漏的问题循环引用只有在容器对象才会产生,比如字典,元组,列表等。首先为了追踪对象,需要每个容器对象维护两个额外的指针,用来将容器对象组成一个链表,指针分别指向前后两个容器对象,这样可以将对象的循环引用摘除,就可以得出两个对象的有效计数,我们通过如下示例,进一步了解一下。

db6d77980f098ee312457a4d03d3ccd8.png

标记-清除机制中,存在root链表unreachable链表,这里简单介绍一下

如上,在未执行 del 语句时,test、ops的引用计数都为 2。但是在 del 执行完以后,test、ops 引用次数互相减 1。test、ops陷入循环引用中,此时标记清除机制来打破这种循环引用,找到其中一端 test 开始拆test、ops的引用环。即从 test 出发,因为它有一个对 ops的引用,则将 ops的引用计数减1,然后顺着引用达到 ops,因为 ops有一个对 test的引用,同样将 test的引用减1,如此就完成了循环引用对象间环摘除。

引用环去掉以后发现,test、ops循环引用变为了0,所以test、ops就被添加到 unreachable链表 中直接被回收掉。


分代回收机制-提升垃圾回收效率

577dd3861865576ea787ffaa45dfeada.png

解决循环引用问题,引入的标记-清除机制,处理过程非常繁琐,需要处理每一个容器对象,因此Python考虑一种改善性能的做法,基于“对象存在时间越长,越不可能在后面的程序中变成垃圾”的假设,提出分代回收机制。出于信任和效率,对于这样一些“寿命长”的对象,我们相信它们的存在价值,所以降低在垃圾回收中扫描它们的频率,分代回收是一种以空间换时间的操作方式。我们可以通过 gc.get_threshold 方法,查看分代回收机制的参数阈值设置,如下:

d9fc70304d5c8c98298ddbaf72a6c7c7.png

Python将所有的对象分为年轻代(第0代)、中年代(第1代)、老年代(第2代)三代。所有的新建对象默认是 第0代对象。当某一代对象经历过垃圾回收,若依然存活,那么它就将被划分到下一代对象。垃圾回收启动时,会扫描所有的 第0代对象。如果 第0代经过一定次数垃圾回收,那么就触发对0代和1代的扫描清理。当第1代也经历了一定次数的垃圾回收后,那么会触发对 第0,1,2代,即对所有对象进行扫描。

如上,gc.get_threshold 方法返回的 (700, 10, 10),700即是垃圾回收启动的阈值,返回的两个10是指,每10次0代垃圾回收,会执行1次1代的垃圾回收;而每10次1代的垃圾回收,会执行1次的2代垃圾回收。

同样可以用 gc.set_threshold 来调整分代回收策略,比如对 第2代对象进行更频繁的扫描,如下:

30047bbf8b8fc8a808ad1d1618feaae0.png

通过此分代回收机制,循环引用中的内存回收处理过程就会得到很大的性能提升。


垃圾回收的时机

b243a1df867ad65d8c9b09757eed1b25.png

在以下三种情况下,会触发垃圾回收机制:

  • 自动回收:当gc模块的计数器达到阀值的时候,触发自动回收。
  • 手动回收:使用gc模块中的collect 方法。
  • 程序退出

接下来我们进一步了解一下 自动回收和手动回收 方式。


自动回收

f0a7dd76f0b407dbb484349ed1016346.png

垃圾回收时,Python不能运行其它的任务,频繁的垃圾回收将极大的降低Python的工作效率,当开启垃圾回收机制时(默认开启),在Python运行过程,会记录其中新增对象和释放对象的次数,当两者的差值高于某个阈值时,自动启动垃圾回收。

在Python中默认开启自动回收,其中涉及方法如下:

  • gc.enable:开启垃圾回收机制(默认开启)。
  • gc.disable:关闭垃圾回收机制。
  • gc.isenabled:判定是否开启。

手动回收

1d7790f72e36f79c4cf3283eab74a63f.png

使用 gc.collect 方法,手动执行分代回收机制。

d3ac826e9f277d66f181cfdc344c53e2.png

上面例子中test、ops为循环引用,通过 gc.collect 手动回收垃圾,实现了回收的两个test、ops的对象。此外,gc.collect 方法返回此次垃圾回收的unreachable(不可达)对象个数,上面例子中回收的两个都是unreachable对象,即清除 我们在标记-清除机制中提到的unreachable 链表中的对象。


3c091e63be42f760f2c3a98a8955eb99.png
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值