Python:垃圾收集器的行为

Posted

技术标签:

【中文标题】Python:垃圾收集器的行为【英文标题】:Python: Behavior of the garbage collector 【发布时间】:2009-11-16 06:20:25 【问题描述】:

我有一个 Django 应用程序,它表现出一些奇怪的垃圾收集行为。特别是有一种观点,每次调用它时都会显着增加 VM 大小 - 达到一定限制,此时使用量再次下降。问题是到达那个点需要相当长的时间,实际上运行我的应用程序的虚拟机没有足够的内存让所有 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.

所以本质上,大量的对象被分配,但最初被移动到第 1 代,当第 1 代在同一个请求期间被扫描时,它们被移动到第 2 代。如果我手动执行 gc.collect(2 ) 之后,它们被删除。而且,正如我所提到的,当下一次自动第 2 代扫描发生时,它也会被删除,如果我理解正确的话,在这种情况下会像每 10 个请求一样(此时应用程序需要大约 150MB)。

好的,所以最初我认为在处理一个请求时可能会发生一些循环引用,从而阻止在处理该请求时收集这些对象中的任何一个。但是,我花了好几个小时尝试使用 pymler.muppy 和 objgraph 找到一个,无论是在请求处理之后还是通过在请求处理中进行调试,似乎都没有。相反,在请求期间创建的大约 14.000 个对象似乎都在某个请求全局对象的引用链中,即一旦请求消失,它们就可以被释放。

无论如何,这一直是我试图解释的。但是,如果这是真的并且确实没有循环依赖关系,那么一旦导致它们被持有的任何请求对象消失,不应该完全释放整个对象树,而没有垃圾收集器参与,纯粹是因为引用计数降为零?

有了这个设置,我的问题如下:

上述内容是否有意义,还是我必须在其他地方寻找问题?在这个特定的用例中,重要数据保留了这么长时间只是一个不幸的意外吗?

我可以做些什么来避免这个问题。我已经看到了一些优化视图的潜力,但这似乎是一个范围有限的解决方案——尽管我也不确定我的通用解决方案是什么;例如,手动调用 gc.collect() 或 gc.set_threshold() 是否明智?

就垃圾收集器本身的工作方式而言:

我是否正确理解一个对象总是被移动到下一代,如果扫描查看它并确定它具有的引用是不是循环的,但实际上可以追踪到根对象。

如果 gc 执行第 1 代扫描,并找到由第 2 代中的对象引用的对象,会发生什么情况;它是在第 2 代内部遵循这种关系,还是在分析情况之前等待第 2 代扫描发生?

在使用 gc.DEBUG_STATS 时,我主要关心“每一代中的对象”信息;但是,我不断收到数百个“gc: 0.0740s elapsed.”、“gc: 1258233035.9370s elapsed”。消息;它们完全不方便——打印出来需要相当长的时间,而且它们使有趣的东西更难找到。有没有办法摆脱它们?

我认为没有办法按代执行 gc.get_objects(),例如,仅检索第 2 代的对象?

【问题讨论】:

【参考方案1】:

以上是否有意义,还是我必须在其他地方寻找问题?在这个特定的用例中,重要数据保留了这么长时间只是一个不幸的意外吗?

是的,确实有道理。是的,还有其他值得考虑的问题。 Django 使用threading.local 作为DatabaseWrapper 的基础(并且一些贡献者使用它来使请求对象可以从没有显式传递的地方访问)。这些全局对象在请求中仍然存在,并且可以保留对对象的引用,直到线程中处理了一些其他视图。

我可以做些什么来避免这个问题。我已经看到了一些优化视图的潜力,但这似乎是一个范围有限的解决方案——尽管我也不确定我的通用解决方案是什么;例如手动调用 gc.collect() 或 gc.set_threshold() 是否明智?

一般建议(可能你知道,但无论如何):避免循环引用和全局变量(包括threading.local)。当 django 设计难以避免时,尝试打破循环并清除全局变量。 gc.get_referrers(obj) 可能会帮助您找到需要注意的地方。另一种禁用垃圾收集器并在每次请求后手动调用它的方法,这是最好的处理方式(这将阻止对象移动到下一代)。

我不认为有办法按代执行 gc.get_objects(),例如,仅从第 2 代检索对象?

不幸的是,gc 接口无法做到这一点。但是有几种方法可以走。您可以只考虑gc.get_objects() 返回的列表末尾,因为此列表中的对象按代排序。您可以通过在调用之间存储对它们的弱引用(例如在WeakKeyDictionary 中)将列表与从先前调用返回的列表进行比较。您可以在自己的 C 模块中重写gc.get_objects()(这很容易,主要是复制粘贴编程!)因为它们是在内部按代存储的,甚至可以使用ctypes 访问内部结构(需要对ctypes 有相当深入的理解)。

【讨论】:

get_objects() 被排序就足够了,感谢提示。【参考方案2】:

我认为您的分析看起来不错。我不是gc 方面的专家,所以每当我遇到这样的问题时,我只需在适当的、非时间关键的地方添加对gc.collect() 的调用,然后就忘了它。

我建议您在视图中调用gc.collect(),看看它对您的响应时间和内存使用有什么影响。

还要注意this question,这表明设置DEBUG=True 会消耗内存,就像它几乎超过了销售日期一样。

【讨论】:

+1 用于提及设置 DEBUG=False 以便 Django 不会记录您的所有 SQL 查询。

以上是关于Python:垃圾收集器的行为的主要内容,如果未能解决你的问题,请参考以下文章

读懂JVM垃圾收集日志

什么时候在python中收集垃圾?

JVM垃圾收集器-ParNew收集器

JVM垃圾收集器-ParNew收集器

JVM 垃圾收集器选择

JVM 垃圾收集器选择