Python:垃圾回收器的行为

9
我有一个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),它们将被删除。 此外,正如我提到的那样,当下一个自动gen 2扫描发生时,它们也会被删除,在这种情况下,每隔大约10个请求(此时应用程序需要大约150MB)。
好的,最初我认为在处理一个请求时可能存在某些循环引用,阻止任何这些对象在处理该请求时被收集。 然而,我花了几个小时使用pympler.muppy和objgraph查找其中一个,包括请求处理内部的调试,但似乎没有。 相反,似乎在请求期间创建的大约14,000个对象都在参考链中与某个请求全局对象相关联,即一旦请求消失,它们就可以被释放。
无论如何,这是我的解释尝试。 但是,如果确实如此,并且确实没有循环依赖关系,那么不应该通过引用计数降至零而不涉及垃圾回收器的情况下释放所有对象树吗?
有了这个设置,以下是我的问题:
  • 上述内容是否有意义,还是我必须在其他地方寻找问题? 在这种特定用例中保留重要数据是否只是不幸的事故?

  • 有什么可以避免此问题的方法吗? 我已经看到了优化视图的潜力,但似乎这只是一个范围有限的解决方案-尽管我不确定通用解决方案是什么;例如,手动调用gc.collect()或gc.set_threshold()有多可取?

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

  • 我理解得对吗,如果一次垃圾回收遍历一个对象并确定它的引用不是循环引用,而实际上可以追溯到一个根对象,那么该对象总是会被移动到下一代。

  • 如果垃圾回收进行了一次例如一代的扫描,并且发现一个对象被二代中的对象引用。那么它是否会在二代内跟进这个关系,或者等待二代的扫描再分析情况?

  • 当使用gc.DEBUG_STATS时,我主要关心每个代中的“对象数量”信息。然而,我经常会看到“gc: 0.0740秒耗时。”、“gc: 1258233035.9370秒耗时。”等数百条消息。它们非常不方便——需要相当长的时间才能打印出来,而且让有趣的事情更难找到。有没有办法摆脱它们?

  • 我不知道是否有一种方式可以按代数执行gc.get_objects(),例如只检索来自第二代的对象?

2个回答

3
上面的内容是有意义的吗,还是我需要在其他地方寻找问题?在这种特定情况下,保留重要数据那么长时间只是不幸的事情吗?
是的,它是有意义的。是的,还有其他值得考虑的问题。 Django使用threading.local作为DatabaseWrapper的基础(一些contribs使用它使请求对象可以从未明确传递的位置访问)。这些全局对象会存活于请求之间,并且可以保留对对象的引用,直到线程中处理了其他视图。
有什么办法可以避免这个问题吗?我已经看到了优化视图的潜力,但这似乎是一个范围有限的解决方案 - 虽然我不确定通用解决方案是什么;例如手动调用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()被排序就足够了,谢谢提示。 - miracle2k

2

我认为你的分析看起来很有道理。我不是gc的专家,所以每当我遇到这样的问题时,我只需在适当的非时间关键位置添加调用gc.collect(),然后就忘记它。

我建议您在视图中调用gc.collect(),并查看其对响应时间和内存使用情况的影响。

请注意这个问题,它表明将DEBUG=True设置会像几乎过期的食品一样占用内存。


在 Django 中,将 DEBUG 设置为 False 可以避免记录所有 SQL 查询,这是一个很好的编程习惯。 - Kekoa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接