Python:垃圾收集器的行为

Python:垃圾收集器的行为,python,django,memory,garbage-collection,Python,Django,Memory,Garbage Collection,我有一个Django应用程序,它表现出一些奇怪的垃圾收集行为。特别是有一种观点认为,每次调用虚拟机时,它都会显著增加虚拟机的大小,直到达到某个限制,此时使用率会再次下降。问题是,要达到这一点需要相当长的时间,而事实上,运行我的应用程序的虚拟机没有足够的内存让所有FCGI进程占用它们有时占用的内存 在过去的两天里,我一直在研究这个问题,并学习Python垃圾收集,我想我确实理解了现在正在发生的事情——大部分情况下。使用时 gc.set_debug(gc.DEBUG_STATS) 然后,对于单个请

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

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

不管怎样,这就是我试图解释它的原因。然而,如果这是真的,并且确实没有循环依赖关系,那么当导致它们被保留的任何请求对象消失时,整个对象树不应该被释放,而不涉及垃圾收集器,纯粹是因为引用计数下降到零吗

在这种设置下,我的问题如下:

  • 以上这些有意义吗?还是我必须从别处寻找问题?在这个特定的用例中,重要的数据被保存了这么长时间,这仅仅是一个不幸的事故吗

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

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

  • 我是否正确理解,如果扫描查看对象并确定其引用不是循环的,但实际上可以跟踪到根对象,则该对象始终会移动到下一代

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

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

  • 我不认为有一种方法可以按代执行gc.get_objects(),例如,只从第2代检索对象


    • 我认为你的分析看起来很正确。我不是
      gc
      方面的专家,因此每当我遇到这样的问题时,我只需在适当的、非时间关键的位置添加对
      gc.collect()
      的调用,然后忘掉它

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

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

      以上这些有意义吗?还是我必须从别处寻找问题?在这个特定的用例中,重要的数据被保存了这么长时间,这仅仅是一个不幸的事故吗

      是的,这是有道理的。是的,还有其他值得考虑的问题。Django使用
      threading.local
      作为
      DatabaseWrapper
      的基础(一些contrib使用它使请求对象可以从未显式传递的位置访问)。这些全局对象在请求之后仍然存在,并且可以保留对对象的引用,直到线程中处理其他视图为止

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

      一般建议(也许你知道,但无论如何):避免循环引用和全局引用(包括
      threading.local
      )。当django design难以避免时,尝试打破循环并清除全局
      gc.get\u referers(obj)
      可能会帮助您找到需要注意的地方。另一种方法是禁用垃圾收集器,并在每次请求后手动调用它,此时它是执行此操作的最佳位置(这将防止对象移动到下一代)

      我不认为有一种方法可以按代执行gc.get_objects(),例如,只从第2代检索对象

      不幸的是,这在
      gc
      接口中是不可能的。但也有一些问题