Python 编程系列十九:分析内存使 在优化应用程序时可能遇到的另一个问题是内存消耗。如果一个程序开始消耗了很多的内存系统就会开始交换在你的应用程序中可能有一个地方有太多的对象被创建或者你不打算保留的对象由于一些无意的引用仍然保持存活。使用传统的分析可以很容易检测到该问题因为消耗很多的内存会引起系统交换会涉及大量 CPU 的工作这很容易被检测到。但有时它不明显必须分析内存使用情况。Python 如何处理内存当你使用 CPython 实现时内存使用情况可能是 Python 中最难分析的事情。虽然像 C这样的语言允许你获得任何元素的内存大小但 Python 永远不会让你知道一个给定的对象消耗了多少内存。这是由于语言的动态特性实际上语言的使用者不能直接访问内存管理器。内存管理的一些原始细节已经在第 7 章中解释过了。我们已经知道 CPython 使用引用计数来管理对象分配。这是确定性算法它确保当对象的引用计数变为 0 时将会触发对象释放。尽管是确定性的这个过程难以手动跟踪而且代码库也非常的复杂。此外在引用计数级别上释放对象并不一定意味着解释器释放了实际的进程的堆内存。根据 CPython解释器的编译标志系统环境或运行时上下文内部内存管理层可能会决定留下一些空闲内存块用于将来重新分配而不是完全释放。CPython 实现中的其他微优化也使预测实际内存使用更加困难。例如指向同一短字符串或小整数值的两个变量可能或可能不指向内存中的同一对象实例。尽管相当吓人看似复杂Python 中的内存管理有着非常好的文档参考 https://docs.python.org/3/c-api/memory.html。注意在大多数情况下在调试内存问题时可以忽略前面提到的微优化。此外引用计数大致基于一个简单的语句如果给定的对象不再被引用它被删除。换句话说函数中的所有本地引用都会被解释器删除。• 离开函数。• 确保对象不再使用。因此保留在内存中的对象如下。• 全局对象。• 仍以某种方式引用的对象。小心参数输入输出的边缘情况。如果在参数中创建了一个对象并且函数返回该对象那么参数引用将仍然存在。如果将其作为默认值使用可能会导致以下意外结果def my_function(argument{}): # 不良实践… if ‘1’ in argument:… argument[‘1’] 2… argument[‘3’] 4… return argument…my_function(){‘3’: 4}res my_function()res[‘4’] ‘I am still alive!’print my_function(){‘3’: 4, ‘4’: ‘I am still alive!’}这就是为什么应该总是像这样使用不可变对象如下def my_function(argumentNone): # 良好的实践… if argument is None:… argument {} # 每次都是创建新的字典… if ‘1’ in argument:… argument[‘1’] 2… argument[‘3’] 4… return argument…my_function(){‘3’: 4}res my_function()res[‘4’] ‘I am still alive!’print my_function(){‘3’: 4}Python 中的引用计数很方便你无需手动跟踪对象的对象引用因此你不必手动销毁它们。虽然这引入了另一个问题但是开发人员从来不需要清除内存中的实例如果开发人员不注意他们使用数据结构的方式内存可能会以不受控制的方式增长。通常消耗内存的情况主要有如下几种。• 不受控制的缓存。• 全局注册实例并且不跟踪其使用情况的对象工厂例如每次调用查询时即时使用的数据库连接器创建者。• 未正确结束的线程。• 使用__del__方法并涉及循环的对象也是内存消费者。在旧版本的 Python在 3.4版本之前垃圾回收器不会打破循环因为它不能确定应该首先删除哪个对象。因此这会导致泄漏内存。在大多数情况下不应该使用此方法。不幸的是在使用 Python/C API 的 C 扩展中引用计数的管理必须使用 Py_INCREF()和 Py_DECREF()宏手动完成。我们在第 7 章中讨论了处理引用计数和引用所有权的注意事项所以你应该已经知道这是一个相当棘手的话题带有各种陷阱。这就是大多数内存问题是由 C 扩展引起的语音这些扩展编写的不太合理。分析内存在开始寻找 Python 中的内存问题之前你应该知道 Python 中内存泄漏的本质是相当特别的。在某些编译语言如 C 和 C 中内存泄漏多数只是由不再被任何指针引用的已分配内存块引起的。如果你没有引用内存你不能释放它这种情况被称为内存泄漏memory leak。在 Python 中没有为用户提供底层的内存管理所以我们宁愿处理泄漏的引用即对不再需要但未被删除的对象的引用。这会阻止解释器释放资源但是与 C 中的内存泄漏的情况不同。当然总是有 C 扩展的例外情况但是它们是不同类型的语言需要完全不同的工具链不能从 Python 代码轻松检查。因此Python 中的内存问题主要是由意外或计划外的资源获取模式引起的。它很少发生这是受到一个真实的 bug 的影响该 bug 是由处理内存分配和重新分配例程不当造成的。当使用 Python/C API 编写 C 扩展时这样的例程仅在 CPython 中可供开发人员使用并且你很少会处理它们。因此Python 中大多数所谓的内存泄漏主要是由软件的过度复杂性和其组件之间的微小交互造成的这些交互真的很难跟踪。为了发现和查找软件的这些缺陷你需要知道在程序中的实际内存使用情况。获取有关 Python 解释器控制的对象数量以及它们的实际大小的信息有点棘手。例如知道给定对象占用多少字节这将涉及其所有属性处理交叉引用然后总结这一切。如果你考虑对象彼此相关的方式这是一个很困难的问题。gc 模块不会为此提供高级函数并且需要 Python 在调试模式下编译从而可以拥有一整套信息。通常程序员只是在执行给定操作之后和之前向系统查询其应用程序的内存使用情况。但是这种测量是一个近似值并且很大程度上取决于系统级的内存管理方式。例如使用Linux 下的 top 命令或 Windows 下的任务管理器可以检测到明显的内存问题。但这种方法太费力并且也难以跟踪错误的代码块。幸运的是有几个工具可以抓取内存快照并计算加载对象的数量和大小。但是让我们谨记Python 不会轻易释放内存而倾向于继续持有以防再次需要。有一段时间调试内存问题和在 Python 中使用的最流行的工具之一是 Guppy-PE 及其Heapy 组件。不幸的是它似乎已不再维护并且缺乏对 Python 3 的支持。幸运的是还有一些其他的选择并且在某种程度上兼容 Python 3。• Memprofhttp://jmdana.github.io/memprof/该工具声明支持 Python 2.6,2.7,3.1,3.2和 3.3 以及一些符合 POSIX 的系统Mac OS X 和 Linux。• memory_profilerhttps://pypi.python.org/pypi/memory_profiler该工具声明支持与Memprof 相同的 Python 版本和系统。• Pymplerhttp://pythonhosted.org/Pympler/该库声明支持 Python 2.5,2.6,2.7,3.1,3.2,3.3和 3.4并且与操作系统无关。请注意上述信息纯粹基于最新分发包中的特性包的分类器。在本书写作之后这可能发生改变。尽管如此当前有一个包支持最广泛的 Python 版本并且在 Python 3.5 下也可以完美无缺地工作。它就是 objgraph。它的 API 似乎有点笨拙并且具有非常有限的功能集。 但它工作在需要它的地方做得很好并且很容易使用。内存工具无需永久添加到生产代码中所以这个工具不需要多漂亮。由于其在操作系统独立性上对 Python 版本的广泛支持所以在讨论内存分析的示例时我们将仅关注 objgraph。本节中提到的其他工具也是令人兴奋的软件但你需要自己研究它们。objgraphobjgraph参考 http://mg.pov.lt/objgraph/是一个简单的工具用于创建对象引用的图表可以用于在 Python 中寻找内存泄漏。它在 PyPI 上可用但它不是一个完全独立的工具需要 Graphviz 才能创建内存使用图。对于像 Mac OS X 或 Linux 这样的开发者友好系统你可以使用首选的系统软件包管理器轻松获取它。对于 Windows你需要从项目页面下载 Graphviz 安装程序参考 http://www.graphviz.org/并手动安装。objgraph 提供了多个实用程序可以列出并且打印有关内存使用情况和对象计数的各种统计信息。在解释器会话中使用此类实用程序的示例如下import objgraphobjgraph.show_most_common_types()function 1910dict 1003wrapper_descriptor 989tuple 837weakref 742method_descriptor 683builtin_function_or_method 666getset_descriptor 338set 323member_descriptor 305objgraph.count(‘list’)266objgraph.typestats(objgraph.get_leaking_objects()){‘Gt’: 1, ‘AugLoad’: 1, ‘GtE’: 1, ‘Pow’: 1, ‘tuple’: 2, ‘AugStore’: 1,‘Store’: 1, ‘Or’: 1, ‘IsNot’: 1, ‘RecursionError’: 1, ‘Div’: 1, ‘LShift’:1, ‘Mod’: 1, ‘Add’: 1, ‘Invert’: 1, ‘weakref’: 1, ‘Not’: 1, ‘Sub’: 1,‘In’: 1, ‘NotIn’: 1, ‘Load’: 1, ‘NotEq’: 1, ‘BitAnd’: 1, ‘FloorDiv’:1, ‘Is’: 1, ‘RShift’: 1, ‘MatMult’: 1, ‘Eq’: 1, ‘Lt’: 1, ‘dict’: 341,‘list’: 7, ‘Param’: 1, ‘USub’: 1, ‘BitOr’: 1, ‘BitXor’: 1, ‘And’: 1,‘Del’: 1, ‘UAdd’: 1, ‘Mult’: 1, ‘LtE’: 1}如前所述objgraph 可以创建内存使用模式和交叉引用的图表交叉引用连接了给定命名空间中的所有对象。该库中最有用的图表实用程序是 objgraph.show_refs()和objgraph.show_backrefs()。它们都接受对被检查对象的引用并使用 Graphviz包将图表图像保存到文件。这样的图的示例在图 11-2 和图 11-3 中示出。以下是用于创建这些图表的代码import objgraphdef example():x []y [x, [x], dict(xx)]objgraph.show_refs((x, y),filename‘show_refs.png’,refcountsTrue)objgraph.show_backrefs((x, y),filename‘show_backrefs.png’,refcountsTrue)ifname “main”:example()图 11-2 显示了 x 和 y 对象保存的所有引用。从上到下和从左到右它提供了 4 个对象• y [x, [x], dict(xx)] 列表实例。• dict(xx) 字典实例。• [x] 列表实例。• x [] 列表实例。为了展示如何在实践中使用 objgraph让我们回顾一些实际的例子。正如我们在本书中已经提到过几次的CPython 有自己的垃圾收集器它独立存在于引用计数方法。它不用于通用内存管理而只用于解决循环引用的问题。在许多情况下对象可能以一种方式互相引用这时使用基于跟踪引用的数量的简单技术无法删除它们。这里是最简单的例子x []y [x]x.append(y)当这种周期中的至少一个对象具有定义的自定义__del__()方法时真正的问题开始了。它是一个自定义的释放处理程序当对象的引用计数最终为零时将调用该方法。它可以执行任何 Python 代码所以它也可以创建特征对象的新引用。这是导致下述问题的原因如果至少有一个对象提供了自定义的__del__()方法实现Python 3.4 版本之前的垃圾回收器就不能中断循环引用。PEP 442 向 Python 引入了对象安全终结从 Python 3.4 开始它已经是标准的一部分。无论如何对包来说这可能仍然是一个问题这些包担心向后兼容性和目标广泛的Python 解释器版本。以下代码段显示了在不同 Python 版本中循环垃圾回收器的行为差异import gcimport platformimport objgraphclass WithDel(list):“” 列出子类中的自定义__del__实现 “”defdel(self):passdef main():x WithDel()y []z []x.append(y)y.append(z)z.append(x)del x, y, zprint(“unreachable prior collection: %s” % gc.collect())print(“unreachable after collection: %s” % len(gc.garbage))print(“WithDel objects count: %s” %objgraph.count(‘WithDel’))ifname “main”:print(“Python version: %s” % platform.python_version())print()main()当在 Python 3.3 下执行时上述代码的输出表明较老版本的 Python 中的循环垃圾收集器不能收集具有__del__()方法定义的对象如下所示$ python3.3 with_del.pyPython version: 3.3.5unreachable prior collection: 3unreachable after collection: 1WithDel objects count: 1使用较新版本的 Python垃圾收集器可以安全地处理终结对象即使它们定义了del()方法如下所示$ python3.5 with_del.pyPython version: 3.5.1unreachable prior collection: 3unreachable after collection: 0WithDel objects count: 0虽然在最新的 Python 版本中自定义终结不再那么棘手但它仍然对需要在不同环境下工作的应用程序造成了一个问题。如前所述objgraph.show_refs()和 objgraph.show_backrefs()函数允许你轻松地发现有问题的类实例。例如我们可以很容易地修改 main()函数以显示对 WithDel 实例的所有反向引用以便查看是否存在泄漏的资源如下所示def main():x WithDel()y []z []x.append(y)y.append(z)z.append(x)del x, y, zprint(“unreachable prior collection: %s” % gc.collect())print(“unreachable after collection: %s” % len(gc.garbage))print(“WithDel objects count: %s” %objgraph.count(‘WithDel’))objgraph.show_backrefs(objgraph.by_type(‘WithDel’),filename‘after-gc.png’)在 Python 3.3 下运行前面的示例将产生一个图见图 11-5它显示 gc.collect()编译代码中处理内存泄漏每个程序员都应该掌握这个工具。它是一个用于构建动态分析工具的完整的探测框架。因此它可能不容易学习和掌握但你应该了解一些基础知识。