引言虽然系统中存在用于清理无用对象的垃圾回收机制, 这并不代表程序就绝对不会出现内存泄漏的情况, 此言确实只对了一半。尽管现在有垃圾回收这一机制存在, 但是在一些特定的情况之下, 它还是会悄悄地把你电脑里的内存给全部用完。这篇文章利用在生产环境里实际发生的大约3个真实案例, 教大家怎么找到问题并且把内存泄漏的情况解决掉。我们常常会发现内存泄漏这个问题, 而它的一个非常普遍的罪魁祸首就是循环引用。class Node:def __init__(self, value):self.value valueself.parent Noneself.children []# 创建循环引用root Node(root)child Node(child)root.children.append(child)child.parent root# 即使删除 rootchild 仍持有 root 的引用# root 和 child 互相引用引用计数永远不会降为 0del rootdel child# 这两个对象需要靠 GC 的循环检测来回收效率较低其垃圾回收组件会借助分代回收这一技术手段, 去应对循环引用问题, 不过这个处理方式并非具备实时性特征, 并且在遇到特定情况时, 可能出现功能失效的情形:import gcprint(gc.get_threshold()) # (700, 10, 10) — 各代触发阈值print(gc.get_count()) # 当前各代对象数量gc.collect() # 手动触发垃圾回收2.这是一个与循环引用相结合的, 具有致命性的组合。class BadClass:def __del__(self):print(f{self} 被回收) # 如果存在循环引用__del__ 永远不会被调用# 当 __del__ 存在且对象之间存在循环引用时# GC 无法确定调用 __del__ 的顺序导致对象被搁置建议的方法是,尽量避免将这两种特性同时应用到代码里, 要是真的不得不这么做的话, 可以考虑使用备选方案来缓解问题。3. 这个全局对象没有受到任何限制, 并且出现了一直不断生长的情况。# 典型的缓存泄漏cache {}def get_data(key):if key not in cache:cache[key] expensive_computation(key) # 缓存只增不减return cache[key]这里展示了一个实战案例, 具体内容就是针对日志处理这个服务出现的内存泄漏问题, 进行排查并且完成修复的操作情况。现象: 日志处理程序在运行了好几个小时之后, 出现了内存溢出错误。# 问题代码class LogProcessor:def __init__(self):self.events [] # 持续追加从不清理def process(self, log_entry):event self.parse(log_entry)self.events.append(event)return event排查工具import tracemalloctracemalloc.start()# ... 运行你的代码 ...snapshot tracemalloc.take_snapshot()top_stats snapshot.statistics(lineno)print(内存占用 Top 10:)for stat in top_stats[:10]:print(stat)修复方案是采取使用双端队列这个方法来对数据的容量大小加以限制, 又或者是选择把数据进行定期的归档操作以及清理工作。from collections import dequeclass LogProcessor:def __init__(self, max_events10000):self.events deque(maxlenmax_events) # 自动丢弃旧数据def process(self, log_entry):event self.parse(log_entry)self.events.append(event)return event第二个案例是出现了滥用的状况, 进而导致相关的对象永远都不会被释放掉。# 问题代码import osclass TempFile:def __init__(self, path):self.path pathself.fd os.open(path, os.O_RDWR | os.O_CREAT)def __del__(self):os.close(self.fd)os.unlink(self.path)# 如果 TempFile 对象间形成循环引用__del__ 永远不会被调用# 导致文件描述符泄漏 磁盘空间泄漏对代码进行修复, 将原有的处理方式替换为使用上下文管理器来实现。class TempFile:def __init__(self, path):self.path pathdef __enter__(self):self.fd os.open(self.path, os.O_RDWR | os.O_CREAT)return selfdef __exit__(self, *args):os.close(self.fd)os.unlink(self.path)return Falsewith TempFile(/tmp/myfile.tmp) as tf:# 使用临时文件pass# 自动清理不依赖 GC案例三C扩展中的内存泄漏# 使用 objgraph 排查import objgraph# 运行一段时间后查看最常见的对象类型objgraph.show_most_common_types(limit20)# 查看某个类型的增长趋势import gcgc.collect()before len([obj for obj in gc.get_objects() if isinstance(obj, MyClass)])# ... 运行代码 ...gc.collect()after len([obj for obj in gc.get_objects() if isinstance(obj, MyClass)])print(fMyClass 实例数量: {before} → {after})# 可视化引用链找出谁在持有泄漏对象objgraph.show_backrefs([leaked_object], max_depth5, filenameleak.png)在对比了那些用于排查内存泄漏的工具之后, 我们应该遵循一些最佳实践, 以防止出现内存泄漏的问题。我们要优先使用 with 语句来管理外部资源, 不要依赖其他方式。在处理父子引用等情况时, 如果要存在循环引用, 那就不要用强引用。给缓存设置好上限, 然后利用 LRU 策略进行淘汰工作。避免在代码中做复杂操作, 特别是那些涉及其他对象引用的行为。定期监控生产环境中的内存使用趋势。# 使用 LRU 缓存避免无限增长from functools import lru_cachelru_cache(maxsize128)def expensive_query(query_id: int):return db.execute(fSELECT * FROM data WHERE id {query_id})总结虽然程序里面自带的垃圾回收功能会帮忙清理掉一些不要的数据, 不过在遇到以下几种场景的时候, 大家依然要特别小心注意:如果你能够灵活使用这三个被称为是神器的手段, 那么在处理内存相关问题的过程中, 你就能够使事情变得事半功倍一些。