Hello,各位小伙伴们周六快乐呀~不知不觉又到年末了,时间过的好快啊。各位打工人,每天都要奥利给哦!!!本期我们聊聊作为高级编程语言的python,它是如何进行内存管理的?
作为一名互联网打工个人,想必大家都知道一个程序在运行时都会在内存中开辟出一块空间,用于存放运行时产生的临时变量。程序执行完成后,再将计算结果输出到硬盘中。如果数据量太大的话,就会很容易出现爆内存(OOM)的尴尬情况。关于这点,我想用自己笔记本电脑跑过深度学习算法的同学肯定明白那种痛苦,简直是难受想哭~
python作为一门高级编程语言,它不像C/C++语言那样具有完善的内存管理机制。针对不会再次用到的内存空间,python语言又是通过啥机制来回收这些内存空间的呢?要想探究这背后的原理,还是得从引用计数开始谈起.......
1.从一个简单的demo程序说起
学过python语言的同学,大多都会听到过“python中一切皆对象”这句话。那么,如何知道一个对象是否永远都不能被调用了呢?答案是:使用这个对象的引用计数,当引用计数为0时,就说明这个对象已经变成垃圾,需要被回收不能再占内存啦。下面,我们通过一个demo程序来分析一波。
import os import psutil # 显示当前python程序占用的内存大小 def show_memory_info(info): pid = os.getpid() p = psutil.Process(pid) tmp = p.memory_full_info() memory = tmp.uss / 1024. / 1024 print("{} memory used: {} MB".format(info, memory)) def bar(): show_memory_info('Initial') a = [i for i in range(100000)] show_memory_info('After a created') bar() show_memory_info('finished') ################## 输出结果 ############# Initial memory used: 8.89453125 MB After a created memory used: 47.5078125 MB finished memory used: 8.8828125 MB
通过上面的demo程序,我们可以发现调用函数bar()时,由于在函数内部创建了列表a,内存空间被迅速占用。而在函数调用结束后,内存又会被重新释放掉。根本原因:由于函数内部声明的列表a是局部变量,在函数返回后就会被销毁。此时,列表a所指代对象的引用计数为0,python便会执行垃圾回收过程。于是,之前被占用的大量内存空间又被释放了。
现在,假设我们将列表a声明为一个全局变量,那么它的生命周期不会因函数bar()执行结束而被回收。列表的引用仍然会存在,因此依然会占用大量内存。
import os import psutil # 显示当前python程序占用的内存大小 def show_memory_info(info): pid = os.getpid() p = psutil.Process(pid) tmp = p.memory_full_info() memory = tmp.uss / 1024. / 1024 print("{} memory used: {} MB".format(info, memory)) def bar(): show_memory_info('Initial') global a # # 即使函数返回后,列表的引用依然存在,于是对象就不会被垃圾回收掉,依然占用大量内存 a = [i for i in range(100000)] show_memory_info('After a created') bar() show_memory_info('finished') ############ 输出结果 ############# Initial memory used: 8.92578125 MB After a created memory used: 47.53125 MB finished memory used: 47.53515625 MB
2.引用计数机制
在python程序中,可以使用sys模块中的getrefcount()函数查看一个变量的引用次数,注意:getrefcount()函数本身也会引入一次计数。另一个需要注意的是:在函数调用发生时,会产生额外的两次引用,一次来自函数栈,另一次来自函数参数。
import sys a = [] # 两次引用,一次来自getrefcount,一次来自a print(sys.getrefcount(a)) def foo(a): # 四次引用:a、python的函数调用栈、函数参数、getrefcount() print(sys.getrefcount(a)) foo(a) # 两次引用,一次来自a,一次来自getrefcount,函数foo()调用已经不存在了 print(sys.getrefcount(a)) # sys.getrefcount()函数可以查看一个变量的引用次数,但getrefcount自身也会引入一次计数 # 注意:发生函数调用时,会额外产生两次引用,一次来自函数调用栈,另一次来自函数参数 a = [] print(sys.getrefcount(a)) # 两次 b = a print(sys.getrefcount(a)) # 三次 c = b d = b e = c f = e g = d # 八次,a、b、c、d、e、f、g这些变量全部指向的是同一个对象,而sys.getrefcount()函数并不是统计一个指针,而是要统计一个对象被引用的次数,因此最后一共有8次引用。 print(sys.getrefcount(a))
C/C++语言中你需要通过free()/delete()函数来手动释放内存,但python中自带的垃圾回收机制会自动释放不会用到的内存空间,对程序员们来说简直是太方便啦。但是,有人就想手动去释放内存也是可以的。你只需要先del a来删除对象的引用,然后强制调用gc.collect()清除没有引用的对象,就可以手动进行垃圾回收处理。
import gc show_memory_info('Initial') a = [i for i in range(1000000)] show_memory_info('After a created') del a gc.collect() show_memory_info('finished') print(a) # 报错 ########### 结果 ############ Initial memory used: 9.078125 MB After a created memory used: 47.671875 MB finished memory used: 9.078125 MB Traceback (most recent call last): File "C:\Users\Administrator\Desktop\garbage_collection.py", line 89, in <module> print(a) # 报错 NameError: name 'a' is not defined
3.循环引用
其实上面的内容也比较容易理解,但是进一步探究引用计数为0与启动垃圾回收两者之间的关系,就会发现两者之间的关系并非是充要条件。思考如下场景:假设两个对象它们互相引用,并且不再被别的对象所引用,那么它们应该被垃圾回收掉吗?
def show_memory_info(info): pid = os.getpid() p = psutil.Process(pid) tmp = p.memory_full_info() memory = tmp.uss / 1024. / 1024 print("{} memory used: {} MB".format(info, memory)) def bar(): show_memory_info('Initial') a = [i for i in range(1000000)] b = [i for i in range(1000000)] show_memory_info('After a, b created') a.append(b) b.append(a) bar() show_memory_info('finished') ########## 输出 ############## Initial memory used: 8.859375 MB After a, b created memory used: 86.4375 MB finished memory used: 86.44140625 MB
上面的代码中,a、b相互引用。此外,a、b作为局部变量在函数bar()调用结束后,a、b这两个对象在代码层面上来说已经不存在了。但是,从输出结果来看,它们依然有内存。这是因为它们两者之间循环引用,导致它们的引用数都不为0。假设上面的代码出现在实际的生产环境中,a、b一开始时占用的内存空间不大,但长时间运行后占用的内存就会越来越大,后果不堪设想。
python语言本身为了能够解决循环引用带来的问题,可以使用显式的gc.collect()函数来手动启动垃圾回收。其实,python也可以使用标记清除算法和分代收集来启动针对循环引用的自动垃圾回收,感兴趣的小伙伴可以阅读文末的参考文章。
def show_memory_info(info): pid = os.getpid() p = psutil.Process(pid) tmp = p.memory_full_info() memory = tmp.uss / 1024. / 1024 print("{} memory used: {} MB".format(info, memory)) def bar(): show_memory_info('Initial') a = [i for i in range(1000000)] b = [i for i in range(1000000)] show_memory_info('After a, b created') a.append(b) b.append(a) bar() gc.collect() # 针对循环引用,手动启动垃圾回收机制 show_memory_info('finished') ########## 输出 ############## Initial memory used: 8.8125 MB After a, b created memory used: 86.3671875 MB finished memory used: 9.07421875 MB