Python:垃圾收集器的行为

标签 python django memory garbage-collection

我有一个 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)。

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

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

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

  • 上面说的有道理吗,还是我必须在别处寻找问题?在这个特定的用例中,重要数据保留了这么长时间只是一个不幸的意外吗?

  • 有什么办法可以避免这个问题。我已经看到了一些优化 View 的潜力,但这似乎是一个范围有限的解决方案——尽管我也不确定我的通用解决方案是什么;例如,手动调用 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 代检索对象?

最佳答案

Does the above even make sense, or do I have to look for the problem elsewhere? Is it just an unfortunate accident that significant data is kept around for so long in this particular use case?

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

Is there anything I can do to avoid the issue. I already see some potential to optimize the view, but that appears to be a solution with limited scope - although I am not sure what I generic one would be, either; how advisable is it for example to call gc.collect() or gc.set_threshold() manually?

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

I don't suppose there is a way to do a gc.get_objects() by generation, i.e. only retrieve the objects from generation 2, for example?

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

关于Python:垃圾收集器的行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1740394/

相关文章:

python - Django 聚合(求和错误

python - 使用定义的 URLconf,Django 尝试了这些 URL 模式

c++ - 带指针的 std::map:错误的值评估

python - 如何获得最大可能的精度? (Python - 十进制)

python - Plotly:如何在子图中制作无界垂直线?

python - 是否可以使用 pexpect 生成将显示在 bash 历史记录中的 bash shell 命令?

python - 将列表表示为带有双引号的字符串,而不是 JSON 的单引号

Python Selenium WebDriver 如何为 get(url) 函数添加超时

python - 即使路径正确,Django object.image.url 也不显示

c - 为什么需要缓存内存对齐?