Python:垃圾收集器的行为
我有一个Django应用程序,发现它的垃圾回收行为有点奇怪。特别是有一个视图,每次被调用时,虚拟机的内存使用量都会显著增加,直到达到某个限制,然后使用量又会下降。问题是,达到这个限制需要花费相当长的时间,而实际上运行我应用的虚拟机并没有足够的内存来让所有的FCGI进程使用那么多内存。
我花了两天时间研究这个问题,了解Python的垃圾回收机制,现在我大致明白发生了什么。当我使用
gc.set_debug(gc.DEBUG_STATS)
时,对于一个请求,我看到以下输出:
>>> c = django.test.Client()
>>> c.get('/the/view/')
gc: collecting generation 0...
gc: objects in each generation: 724 5748 147341
gc: done.
gc: collecting generation 0...
gc: objects in each generation: 731 6460 147341
gc: done.
[...more of the same...]
gc: collecting generation 1...
gc: objects in each generation: 718 8577 147341
gc: done.
gc: collecting generation 0...
gc: objects in each generation: 714 0 156614
gc: done.
[...more of the same...]
gc: collecting generation 0...
gc: objects in each generation: 715 5578 156612
gc: done.
基本上,分配了大量对象,但最初这些对象被移动到第一代。当第一代在同一个请求中被清理时,它们被移动到第二代。如果我之后手动调用gc.collect(2),这些对象就会被移除。而且,正如我提到的,当下一个自动的第二代清理发生时,它们也会被移除。如果我没理解错的话,这种情况大约每10个请求发生一次(此时应用大约需要150MB的内存)。
起初,我以为在处理一个请求的过程中可能存在某种循环引用,导致这些对象无法在处理该请求时被清理。然而,我花了几个小时尝试使用pympler.muppy和objgraph来查找,甚至在请求处理过程中进行调试,但似乎没有发现任何循环引用。相反,似乎在请求期间创建的约14000个对象都与某个请求全局对象有引用关系,也就是说,一旦请求结束,它们就可以被释放。
这就是我对这个问题的解释。不过,如果这是真的,并且确实没有循环依赖,那么一旦导致这些对象被引用的请求对象消失,整个对象树应该会被释放,而不需要垃圾回收器的参与,仅仅是因为引用计数降到零。
基于这个情况,我有以下几个问题:
以上说法有道理吗?还是我需要在其他地方寻找问题?难道在这个特定的用例中,保留大量数据只是一个不幸的意外吗?
我能做些什么来避免这个问题?我已经看到了一些优化视图的潜力,但这似乎是一个有限的解决方案——虽然我也不确定一个通用的解决方案是什么;例如,手动调用gc.collect()或gc.set_threshold()是否可行?
关于垃圾回收器本身的工作原理:
我理解得对吗?如果一个对象被检查时发现它的引用不是循环的,而且可以追溯到根对象,它就会被移动到下一代。
如果垃圾回收器进行第一代清理,发现一个对象被第二代中的某个对象引用,它会跟踪这个关系,还是要等到第二代清理发生后再分析情况?
使用gc.DEBUG_STATS时,我主要关心的是“每代中的对象”信息;然而,我不断收到数百条“gc: 0.0740s elapsed.”和“gc: 1258233035.9370s elapsed.”的消息;这些消息非常麻烦——打印这些信息需要相当长的时间,而且让有趣的内容更难找到。有没有办法去掉这些消息?
我想知道是否有办法按代获取gc.get_objects(),也就是说,只检索第二代的对象?
2 个回答
我觉得你的分析很有道理。我对 gc
不是很专业,所以每当遇到类似的问题时,我通常会在一个合适的、不太紧急的地方加上 gc.collect()
的调用,然后就不再想它了。
我建议你在你的视图中调用 gc.collect()
,看看它对你的响应时间和内存使用有什么影响。
另外,还要注意这个 问题,它提到设置 DEBUG=True
会像快过期一样消耗内存。
上面的内容有道理吗?还是说我得去别的地方找问题?难道在这个特定的情况下,重要的数据被保留这么久只是个不幸的意外吗?
是的,这确实有道理。而且,还有其他值得考虑的问题。Django使用threading.local
作为DatabaseWrapper
的基础(有些扩展也用它来让请求对象在没有明确传递的地方可用)。这些全局对象在请求之间存活,可以在同一个线程中保持对对象的引用,直到处理其他视图。
有没有什么办法可以避免这个问题?我已经看到一些优化视图的潜力,但这似乎是个有限的解决方案——虽然我也不太确定一个通用的解决方案是什么;比如手动调用gc.collect()或gc.set_threshold()有多可取?
一般建议(你可能已经知道,但还是说一下):避免循环引用和全局变量(包括threading.local
)。尽量打破循环,并在Django的设计让你难以避免时清理全局变量。gc.get_referrers(obj)
可能会帮助你找到需要关注的地方。另一种方法是禁用垃圾回收器,并在每次请求后手动调用它,这样做是最合适的(这将防止对象进入下一代)。
我想知道有没有办法按代数来做gc.get_objects(),也就是说,只获取第二代的对象,比如?
不幸的是,使用gc
接口是做不到的。不过有几种方法可以尝试。你可以只考虑gc.get_objects()
返回的列表的末尾,因为这个列表中的对象是按代数排序的。你可以通过在调用之间存储它们的弱引用(例如,使用WeakKeyDictionary
)来比较当前列表和上次调用返回的列表。你也可以在自己的C模块中重写gc.get_objects()
(这很简单,基本上就是复制粘贴编程!),因为它们在内部是按代数存储的,或者甚至可以使用ctypes
访问内部结构(这需要对ctypes
有相当深入的理解)。