一次python内存调优经历
文章最后更新时间为:2019年08月20日 11:32:15
对于C语言来说,内存泄露是个很常见的事故,因此写代码的时候要格外注意无用内存的释放,但是对于pythoner来说,一般都不会关心这些,因为python会自己去管理内存。
但是最近我遇到一个问题,我在写一个程序,将会占用很大的内存,我先使用了一个集合用来存储数据。
比如下面这样
example_set = set()
for i in range(10000000):
example_set.add(i)
然后我需要将多个数据组合成新的几个:
example_list = []
for data in example_set:
dic = {
"id":random.randint(1,10000)
"data":data
}
example_list.append(dic)
后续的话还需传递:
example_list_1 = example_list
这样以前的数据比如example_set
,example_list
都不再需要了,而他们又是全局变量,又占用很大的空间,我是否需要手动释放这些数据,我尝试了使用del来手动释放这些数据,但是出现了问题,于是学习一下python的内存回收。
1. 实例
首先先用实例感受一下python的内存机制.
我们先定义一个函数用来查看当前进程占用的内存大小:
import os
import psutil
# 显示当前 python 程序占用的内存大小
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))
然后尝试写一个比较大的列表,然后复制一下:
import random
show_memory_info('step1')
a = [random.randint(1,10000) for i in range(1000000)]
show_memory_info('step2')
b = a
show_memory_info('step3')
结果为:
step1 memory used: 9.7578125 MB
step2 memory used: 47.75390625 MB
step3 memory used: 47.78515625 MB
很显然这里的b = a并没有产生更多的内存占用。因为这里的赋值属于对象引用的传递,并不会重新划分内存,而是直接将b指向a指向的内存。
那么不直接赋值,而是合成呢?可以看下面的例子:
import random
show_memory_info('step1')
a = [random.randint(1,10000) for i in range(1000000)]
show_memory_info('step2')
b = []
for i in a:
b.append({'num':i})
show_memory_info('step3')
del a
show_memory_info('step4')
结果:
step1 memory used: 9.3046875 MB
step2 memory used: 47.3046875 MB
step3 memory used: 288.8984375 MB
step4 memory used: 281.265625 MB
第一步申请a占用了47MB内存,但是很显然step4释放了a之后,内存只减少了7MB,这说明a和b其实是有交叉引用的,也就是说a和b指向的内存并不是完全分开的。
这里其实对应了python一切皆对象的说法,a和b只是指针,他们指向内存中的某块区域,用a合成b的时候,并不是重新划分内存来保存b的值,其公共部分还是指向原来的内存。
得出结论,参数传递和重新合成并不会占用更多内存,除非是深拷贝
2. python内存回收
其实从上面的例子中我们已经知道了,一般情况下无需手动释放不需要的变量,那么python是如何释放不必要的内存的呢?
其中最主要的机制是引用计数当一个对象的引用计数为0时,就意味着没有指针会指向这个对象,这个对象自然成了垃圾,需要回收。
看一个例子:
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
sys.getrefcount() 这个函数,可以查看一个变量的引用次数,其自身也会引入一次计数。
在函数调用发生的时候,会产生额外的两次引用,一次来自函数栈,另一个是函数参数。
import sys
a = []
print(sys.getrefcount(a)) # 两次
b = a
print(sys.getrefcount(a)) # 三次
c = b
d = b
e = c
f = e
g = d
print(sys.getrefcount(a)) # 八次
########## 输出 ##########
2
3
8
a、b、c、d、e、f、g 这些变量全部指代的是同一个对象,所以这个对象的最后会有8次引用。
理解引用这个概念后,垃圾回收也就很容易理解了。
如果偏想手动释放内存,可以先调用 del a 来删除一个对象;然后强制调用 gc.collect(),即可手动启动垃圾回收。看下面的例子
import gc
show_memory_info('initial')
a = [i for i in range(10000000)]
show_memory_info('after a created')
del a
gc.collect()
show_memory_info('finish')
print(a)
########## 输出 ##########
initial memory used: 48.1015625 MB
after a created memory used: 434.3828125 MB
finish memory used: 48.33203125 MB
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-12-153e15063d8a> in <module>
11
12 show_memory_info('finish')
---> 13 print(a)
NameError: name 'a' is not defined
除了引用计数外,python的垃圾回收还有其他机制:标记清除+分代收集
其余两种机制还有待学习,因和需求关系不大,鸽了
总结
- python一切皆对象,普通的值的传递只是引用的传递。
- 垃圾回收是 Python自带的机制,用于自动释放不会再用到的内存空间
- 引用计数是其中最简单的实现,这只是充分非必要条件