解码Python中的垃圾回收:如何优化您的代码性能

前言

我们知道,python 是一种高级编程语言,它提供了自动内存管理的功能,即垃圾回收机制。垃圾回收机制是一种自动管理内存的技术,它可以帮助开发者在编写代码时不必关注内存的分配和释放,从而提高开发效率。好奇的同学会问了,python的垃圾回收机制到底是如何实现的呢?带着疑问,我们一起进行探索。

为啥需要垃圾回收?

  • 内存泄漏:在程序运行过程中,如果开发者没有正确地释放不再使用的内存,就会导致内存泄漏。内存泄漏会导致程序占用的内存越来越多,最终可能导致程序崩溃或者系统变得非常缓慢。垃圾回收机制可以自动检测和回收不再使用的内存,避免内存泄漏的问题。
  • 简化内存管理:在一些低级编程语言中,开发者需要手动分配和释放内存,这样容易出现内存泄漏和内存溢出的问题。而垃圾回收机制可以自动管理内存,开发者不需要关注内存的分配和释放,从而提高开发效率。

总之,垃圾回收的存在是为了解决内存泄漏和简化内存管理的问题。它可以自动检测和回收不再使用的内存,避免内存泄漏,并提高开发效率。在高级编程语言中,垃圾回收是一项非常重要的功能。

怎么实现的呢?

Python 的垃圾回收机制主要通过引用计数和循环引用检测来实现。

引用计数

引用计数是一种简单而高效的垃圾回收算法,它通过记录每个对象的引用数量来判断对象是否仍然被使用。当一个对象的引用计数为0时,说明该对象已经不再被使用,可以被回收。接下来,我们利用sys.getrefcount()查看变量的引用次数,这样你一定会清晰很多。

案例一

import sys
​
a = []
​
print(sys.getrefcount(a)) # 2
​
def func(a):
    print(sys.getrefcount(a)) # 4
​
func(a)
​
print(sys.getrefcount(a)) # 2
​

  • 第一个print会输出2,有2次引用,一次来自 a,一次来自 getrefcount
  • 第二个print会输出4,有4次引用,一次来自a,一次来自python 的函数调用栈,一次来自函数参数,一次来自 getrefcount
  • 第三个print会输出2,有2次引用,一次来自a,一次来自 getrefcount

强调一点:在函数调用发生时,会产生额外的2次引用,一次来自函数栈,一次来自函数参数

案例二

我们在举个例子,加深理解

import sys
​
a = []
​
print(sys.getrefcount(a)) # 2
​
b = a
​
print(sys.getrefcount(a)) # 3
​
c = b
d = b
e = c
f = e
g = d
​
print(sys.getrefcount(a)) # 8
​

可以看到a、b、c、d、e、f、g 这些变量全部指代的是同一个对象,这个对象被引用8次,所以最终输出8

案例三

我们看看,未回收和回收后内存的变化。

import os
import psutil
​
​
def show_memory_info(hint):
    pid = os.getpid()
    p = psutil.Process(pid)
​
    info = p.memory_full_info()
    memory = info.uss / 1024. / 1024
    print('{} memory used: {} MB'.format(hint, memory))
    
def func():
    show_memory_info('initial')
    a = [i for i in range(10000000)]
    show_memory_info('after a created')
​
func()
show_memory_info('finished')

我们定义了一个函数show_memory_info用来打印当前python程序占用的内存大小,定义了一个函数func()来创建变量a,在创建变量a之前打印占用内存,最后在函数func()调用销毁后,再次打印内存占用。在看过案例一之后,相信你一定知道,函数内部声明的列表 a 是局部变量,在函数返回后,局部变量的引用会注销掉;此时,列表 a 所指代对象的引用数为 0,Python 便会执行垃圾回收。我们看看执行结果是不是这样:

initial memory used: 30.75 MB
after a created memory used: 415.6328125 MB
finished memory used: 30.98828125 MB

可以看到确实如此。

那我们如果将变量声明为全局变量,这样函数销毁后,列表的引用计数还存在,内存应该还是很大。测试一下:

def func():
    show_memory_info('initial')
    global a
    a = [i for i in range(10000000)]
    show_memory_info('after a created')

执行结果如下:

initial memory used: 30.25390625 MB
after a created memory used: 415.38671875 MB
finished memory used: 415.38671875 MB

可以看到结果是满足预期的。

那如果我们将func()函数生成的列表返回return a,然后调用函数并赋值给一个变量,此时列表引用也会存在,内存不会释放。测试一下

def func():
    show_memory_info('initial')
    a = [i for i in range(10000000)]
    show_memory_info('after a created')
    return a
​
f = func()

执行结果如下:

initial memory used: 30.1875 MB
after a created memory used: 415.0703125 MB
finished memory used: 415.0703125 MB

可以看到,确实还有大量内存被占用。

到这里,应该对引用计数释放内存有一个清晰的认识了吧,现在,有人会问,我确实在某种场景下,需要手动释放内存该怎么办呢?当然python也是支持的,还是上面定义全局变量a的例子,我们只需要最后执行del a,删除对象的引用,然后强制调用gc.collect(),即可手动启动垃圾回收。

循环引用

在上面案例三中,我们提到局部变量,在函数返回后,局部变量的引用会注销掉。看下面这段例子:

def func():
    show_memory_info('initial')
    a = [i for i in range(10000000)]
    b = [i for i in range(10000000)]
    show_memory_info('after a, b created')
    a.append(b)
    b.append(a)
func()
show_memory_info('finished')

按照我们上面学习的,ab都是局部变量,函数返回后,应该引用计数会变为0,内存会释放,我们测试一下:

执行结果如下:

initial memory used: 30.80078125 MB
after a, b created memory used: 801.99609375 MB
finished memory used: 801.99609375 MB

可以看到内存并没有释放,说明ab的引用计数应该不为0。为啥出现这种情况呢?就是因为相互引用。那这种情况怎么解决呢?引用计数最后一部分提到,可以强制调用gc.collect()

什么是循环引用

循环引用是指两个或多个对象之间相互引用,形成一个环状结构。这种情况下,引用计数算法无法正确判断对象是否仍然被使用,因为它们的引用计数永远不会变为0。为了解决循环引用的问题,Python 引入了垃圾回收器,它使用了一种称为标记-清除的算法。

标记-清除算法分为两个阶段:标记阶段和清除阶段。在标记阶段,垃圾回收器会从根对象开始遍历所有可达对象,并将它们标记为活动对象。而在清除阶段,垃圾回收器会遍历整个堆内存,将未标记的对象进行回收。

除了引用计数和标记-清除算法,Python 还使用了分代回收的策略。分代回收是一种基于对象存活时间的优化策略,它将对象分为不同的代,每个代有不同的回收频率。一般来说,新创建的对象会被分配到第0代,而经过一次垃圾回收后仍然存活的对象会被提升到下一代。这样可以减少垃圾回收的频率,提高性能。

如何调试内存泄漏

这里推荐objgraph,是一个可视化引用关系的包。

import objgraph
​
a = [1, 2, 3]
b = [4, 5, 6]
​
a.append(b)
b.append(a)
​
objgraph.show_refs([a])

这里通过show_refs()可以生成清晰的引用关系图。

最后

Python 的垃圾回收机制是一种自动管理内存的技术,它通过引用计数和循环引用检测来判断对象是否仍然被使用,并使用标记-清除算法进行回收。此外,还采用了分代回收的策略来优化性能。了解这些垃圾回收机制的工作原理对于编写高效的 Python 代码非常重要。

如果你对Python感兴趣,想要学习python,这里给大家分享一份Python全套学习资料,都是我自己学习时整理的,希望可以帮到你,一起加油!

😝有需要的小伙伴,可以V扫描下方二维码免费领取🆓

1️⃣零基础入门

① 学习路线

对于从来没有接触过Python的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。
在这里插入图片描述

② 路线对应学习视频

还有很多适合0基础入门的学习视频,有了这些视频,轻轻松松上手Python~
在这里插入图片描述

③练习题

每节视频课后,都有对应的练习题哦,可以检验学习成果哈哈!
在这里插入图片描述

2️⃣国内外Python书籍、文档

① 文档和书籍资料

在这里插入图片描述

3️⃣Python工具包+项目源码合集

①Python工具包

学习Python常用的开发软件都在这里了!每个都有详细的安装教程,保证你可以安装成功哦!
在这里插入图片描述

②Python实战案例

光学理论是没用的,要学会跟着一起敲代码,动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。100+实战案例源码等你来拿!
在这里插入图片描述

③Python小游戏源码

如果觉得上面的实战案例有点枯燥,可以试试自己用Python编写小游戏,让你的学习过程中增添一点趣味!
在这里插入图片描述

4️⃣Python面试题

我们学会了Python之后,有了技能就可以出去找工作啦!下面这些面试题是都来自阿里、腾讯、字节等一线互联网大厂,并且有阿里大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值