一次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_setexample_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自带的机制,用于自动释放不会再用到的内存空间
  • 引用计数是其中最简单的实现,这只是充分非必要条件
1 + 5 =
快来做第一个评论的人吧~