Python内存管理和gc

参考:

最近开始大规模跑数据了,跑着跑着进程就被系统kill掉了,查其原因,原来是OOM(Out of Memory)了,为什么会占这么多的内存呢,我的程序需要计算数万个资金账号,然后每个资金账号可能计算数年,而且我是每天每天算的,所以基本每个资金账号都会循环个几百次,然后几万个资金账号,想想确是吓人。但是Python有垃圾回收机制啊,每个资金账号计算完成后,理应回收掉啊,也不应该出现这个超出内存的问题啊。看来得深入研究一下Python的内存管理机制了,心中隐隐约约的觉得Python不会立即进行垃圾回收,所以导致了内存超限。

对象与引用

a = 1这样整数1是一个对象,a是一个引用,而Python对于这种整数和短小的字符,会缓存这些对象,以便重复使用,如果b = 1那么a和b是引用的同一个对象。

Python里每个对象都存有指向该对象的引用总数,即引用计数,可以使用sys包的getrefcount()来查看某个对象的引用计数。

对于容器对象,里面包含的是元素对象的引用。比如对于列表[1, 2, 3],如果随便改一个元素,就会导致这个元素的引用和以前的不一样,所以列表也会发生变化。但是如果[[1,2], 3],第一个元素是一个列表,而这个列表存的是第一个元素所代表的列表的引用,所以子列表里面的内容发生变化,原列表也会track到。

两个对象之间可能互相引用,形成引用环。

可以使用del关键字来删除某个引用,使得某个对象的引用减少,所以del不能使得释放内存,而只是减少一次引用。

垃圾回收

Python会在适当的时候启动垃圾回收。如果Python某个对象的引用计数为0,则说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。当垃圾回收启动时,Python扫描到这个引用计数为0的对象,就将它所占据的内存清空。垃圾回收时,Python不能进行其它的任务。频繁的垃圾回收将大大降低Python的工作效率。如果内存中的对象不多,就没有必要总启动垃圾回收。所以,Python只会在特定条件下,自动启动垃圾回收。 当Python运行时,会记录其中分配对象(object allocation)和取消分配对象(object deallocation)的次数。当两者的差值高于某个阈值时,垃圾回收才会启动。

我们也可以手动启动垃圾回收,即使用gc.collect()

说到这里,大概就知道自己的程序为什么内存占用这么高了:在开始一次垃圾回收之前,积攒的对象太慢,但是每个对象都会占用相当大的内存,所以基本上还没有来得及释放,就已经满了。比如分配对象和取消分配对象的次数需要5000次才开始回收,但是每个对象占用了2M,那么就是占用10G内存多,8个进程,80G?

后来每个资金账号计算完成后,添加了一句gc.collect(),强制进行垃圾回收,然后再也没有出现过内存占用高的情况了。

分代回收

基本假设是,存活时间越久的对象,越不可能在后面的程序中变成垃圾。我们的程序往往会产生大量的对象,许多对象很快产生和消失,但也有一些对象长期被使用。出于信任和效率,对于这样一些“长寿”对象,我们相信它们的用处,所以减少在垃圾回收中扫描它们的频率。

Python将所有的对象分为0,1,2三代。所有的新建对象都是0代对象。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。垃圾回收启动时,一定会扫描所有的0代对象。如果0代经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理。当1代也经历了一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描。

孤立的引用环

回收这样的引用环,Python复制每个对象的引用计数,可以记为gc_ref。假设,每个对象i,该计数为gc_ref_i。Python会遍历所有的对象i。对于每个对象i引用的对象j,将相应的gc_ref_j减1。

在结束遍历后,gc_ref不为0的对象,和这些对象引用的对象,以及继续更下游引用的对象,需要被保留。而其它的对象则被垃圾回收。

比如对于a和b相互引用,首先遍历到a, 然后gc_ref_j减一等于0,然后遍历b,gc_ref_i减一等于0,然后a和b引用都等于0,所以被回收。

gc

这个模块提供了自定义垃圾收集器的接口。它提供了禁止收集器,调整收集频率,设置debugging选项。并且也提供访问收集器发现但是无法收集的不可达对象。自从收集器在Python中补充了引用计数,在你确保程序中不会产生引用循环的情况下你可以禁止收集器。可以通过gc.disable()来禁止自动收集。来debug一个内存泄漏的程序可以调用gc.set_debug(gc.DEBUG_LEAK)。注意到这一标志包含了gc.DEBUG_SAVEALL,导致垃圾收集对象会在gc.garbage中存储下来用于检查。

下面介绍几个主要常用的方法

  • gc.enable()

开启自动垃圾收集,默认是开着的

  • gc.disable()

关闭自动垃圾收集

  • gc.isenabled()

返回自动垃圾收集是否打开

  • gc.collect(generation=2)

如果没有参数,进行全方位的收集,可选参数generation为整数,来指定取收集哪一代(0-2)。如果给定的代数无效,会引发ValueError,会返回找到的不可达的对象个数。

  • gc.set_debug()

设置垃圾收集的debugging标志。调试信息将会写入到sys.stderr,下面的调试标志可以组合起来用。

  • gc.get_dubug()

返回目前设置的调试标志

  • gc.get_stats()

返回三代每代的包含收集统计信息的字典列表。键的数目在将来可能会改变,但是当前每个字典会包含以下:

collections 是这代被收集过的次数

collected 是在这代总共收集的对象总数

uncollectable 是这代中无法收集的对象总数

  • gc.set_threshold(threshold0[, threshold1[, threshold2]])

设置垃圾收集阈值,即收集频率。设置threshold0为0禁止垃圾收集

GC视对象可以在多少次垃圾收集里存活将对象分成三代。新的对象会分配在第0代。如果一个对象在一次垃圾收集里还会存活,那么会将其移动到更老的一代。2是最老的代,在这里面的对象会保持在这一代。为了决定什么时候去运行垃圾收集,收集器会从上一次收集开始跟踪对象分配数和取消分配数。当两者之差超过了threshold0,就会开始收集。 最开始只有0代会被检查,如果0代检查的次数超过了threshold1,那么1代也开始检查,1代检查次数超过threshold2,那么2代开始检查。

  • gc.garbage

收集器发现的不可达的对象列表。如果DEBUG_SAVEALL被设置,那么就会将这些对象加入列表而不是释放它们。

  • gc.DEBUG_LEAK

调试内存泄漏程序,等于DEBUG_COLLECTABLE | DEBUG_UNCOLLECTABLE | DEBUG_SAVEALL