python 内存管理 & 内存泄漏及排查方案 & 内存友好的python代码
Python 内存管理
一、一句话总结
Python 的内存管理就是三件事:
- 自动分配内存(你不用管变量存在哪)
- 自动回收垃圾(不用的对象自动删掉)
- 靠引用计数 + 分代垃圾回收实现
二、核心机制 1:引用计数(最基础)
是什么?
每个对象都有一个引用计数器,记录有多少个变量指向它。
规则
- 变量指向它 → 计数 +1
- 变量不再指向它 → 计数 -1
- 计数 = 0 → 立刻被回收,内存释放
例子
a = [1,2,3] # 引用计数 = 1
b = a # 引用计数 = 2
del a # 引用计数 = 1
del b # 引用计数 = 0 → 立即回收
优点
- 实时性高
- 不用的对象马上释放内存
缺点
- 无法解决循环引用(这是最大弱点)
三、核心机制 2:垃圾回收(GC)解决循环引用
什么是循环引用?
a = []
b = []
a.append(b)
b.append(a)
a 引用 b,b 引用 a → 互相引用,谁也不释放。
引用计数永远不会变成 0 → 内存泄漏!
所以 Python 引入 GC(垃圾回收) 专门清理这种情况。
四、核心机制 3:分代回收(GC 工作方式)
Python 把对象分成 3 代:
- 0 代:新创建的对象(扫描最频繁)
- 1 代:活过一次GC
- 2 代:活得很久的对象(扫描最少)
规则
- 活得越久,越不可能被删
- 越新的对象越容易死,越要频繁检查
效率极高!
五、内存池机制(提升速度)
Python 不会每次都向操作系统要内存,而是自己维护一个内存池:
- 小块内存 → 从内存池拿
- 用完归还内存池
- 避免频繁申请/释放,速度快很多
六、完整流程(面试必背)
- 创建对象 → 自动分配内存
- 用引用计数跟踪对象被使用次数
- 计数=0 → 立即释放
- 遇到循环引用 → GC分代回收清理
- 小内存使用内存池,提升效率
七、一句话总结
Python 内存管理以引用计数为基础,负责实时回收对象;
通过分代垃圾回收解决循环引用问题;
并使用内存池减少内存开销,提升效率。
Python 内存泄漏 + 排查方案(面试+实战)
一、先懂:什么是内存泄漏
本该被销毁的对象,引用一直没断开,GC回收不掉,内存只涨不跌
常见原因:
- 循环引用没断干净
- 全局变量一直持有大对象
- 长连接/缓存无限堆积不清空
- 线程、定时器、事件监听忘记关闭
- 第三方库/对象手动内存不释放
二、最常见 3 种泄漏场景
1. 循环引用(引用计数失效)
a = []
b = []
a.append(b)
b.append(a)
# 互相引用,计数永远不为0,老版本极易泄漏
现在Python3 GC能自动清理,但大量批量循环引用依旧会卡、占内存
2. 全局变量常驻内存
cache_list = [] # 全局列表
def add_data():
for i in range(100000):
cache_list.append([i]*100) # 无限塞,永不清空
程序运行越久内存越大
3. 类实例/闭包持续持有引用
def func():
big_data = list(range(1000000))
def inner():
print(len(big_data))
return inner
f = func()
# inner闭包一直持有big_data,无法释放
三、4 大排查工具(由简到难)
1. 内置模块 gc 查看回收情况
import gc
# 开启调试,打印回收详情
gc.set_debug(gc.DEBUG_LEAK)
# 手动触发垃圾回收
gc.collect()
# 查看未被回收的泄漏对象
print(gc.garbage)
gc.garbage 里有内容 = 确定存在泄漏对象
2. psutil 实时监控进程内存
import psutil
import os
def get_memory():
pid = os.getpid()
p = psutil.Process(pid)
# 单位 MB
return p.memory_info().rss / 1024 / 1024
print(get_memory())
循环运行代码,观察内存持续上涨不回落=泄漏实锤
3. objgraph 定位泄漏对象(最常用)
安装:
pip install objgraph
用法:
import objgraph
# 查看数量最多的对象
objgraph.show_most_common_types(limit=20)
# 查看某个对象的引用链,找到是谁还在持有
objgraph.show_backrefs(objgraph.find_backref_chain(泄漏对象))
直接追踪:谁在死死抓住这个对象不放
4. memory_profiler 逐行分析内存
安装
pip install memory-profiler
用法:
from memory_profiler import profile
@profile
def test():
# 你的业务代码
pass
test()
运行后逐行显示每行占用内存,精准定位爆内存代码
四、解决内存泄漏 6 个实用方案
- 主动断开引用
不用对象手动赋值None
big_data = None
- 用完及时 del 删除
del a, b
- 清空全局缓存/列表
cache_list.clear()
-
避免闭包长期持有大对象
用完主动解绑 -
手动触发GC
定时执行
import gc
gc.collect()
- 改用弱引用 weakref
不想强持有,用弱引用,不影响回收
import weakref
obj = weakref.proxy(原对象)
五、面试标准回答
Python内存泄漏大多由循环引用、全局常驻对象、闭包强引用、资源未释放导致;
排查可使用 gc 模块查看垃圾对象、psutil 监控进程内存、objgraph 追踪引用链、memory_profiler 逐行分析;
解决方式:主动置空引用、及时删除对象、定期清理缓存、使用弱引用、手动触发垃圾回收。
六、速记口诀
内存只涨不回落,大概率是引用锁;
循环引用全局存,闭包持有甩不脱;
gc查垃圾,psutil看涨跌,objgraph追源头。
如何写出内存友好的 Python 代码?
- 谨慎使用全局变量: 全局变量通常会进入 GC 的第 2 代,长期霸占内存。能放在函数内部的局部变量,就不要放在全局。
避免死结般的循环引用: 如果在设计树状结构或图结构时不可避免地遇到双向引用(比如父节点指向子节点,子节点又指向父节点),请使用标准库中的 weakref(弱引用)模块。弱引用不会增加对象的引用计数。
处理海量数据时使用生成器: 如上一篇文章所说,生成器按需产出数据,不会一次性撑爆内存池。
手动触发 GC(必要时): 在跑完一个极消耗内存的数据清洗脚本后,可以通过 import gc; gc.collect() 强制执行一次全量垃圾回收,清理内存碎片。
深入底层,才能在架构和性能优化时游刃有余。Python 绝不仅仅是简单的脚本语言,它的内部设计同样充满了权衡与智慧。
一、核心原则
少创建、晚创建、及时释放、按需加载、避免常驻
二、变量与对象优化
- 不用就置空 / del 销毁
# 用完大对象立刻解绑
large_data = None
del large_data
- 优先局部变量,少用全局变量
全局变量生命周期贯穿整个程序,长期占内存;局部函数执行完自动释放。 - 避免无意识创建临时大对象
# 差:生成完整新列表
new_list = [x*2 for x in big_list]
# 好:用生成器,不占整块内存
gen = (x*2 for x in big_list)
- 小数据复用对象,减少频繁新建
循环内不要反复创建大容器,提前初始化复用。
三、容器选型(最关键)
- 海量遍历优先用 生成器 / 迭代器
列表一次性载入全量数据,生成器边用边生成,几乎不占内存
# 占内存
nums = list(range(10000000))
# 内存友好
nums = range(10000000)
- 大量键值存储:按需选结构
- 普通字典够用就不用嵌套多层字典
- 固定结构数据用
namedtuple/dataclass更省内存 - 数值矩阵优先 numpy 数组,比原生列表省超多内存
- 大列表及时清空
data_list.clear()
四、循环写法优化
- 避免循环内嵌套循环生成大中间数据
- 遍历优先 for-in 迭代,不要先切片复制
# 浪费:复制一份新列表
for item in arr[:]:
pass
# 友好:直接遍历原数组
for item in arr:
pass
- 分批处理大数据,不要一次性全读入
文件、日志、数据库数据分页/分块读取,而非一次性加载到内存。
五、字符串优化
- 大量拼接不用
+=
频繁字符串拼接不断新建对象,改用列表收集最后join
# 差
s = ""
for i in range(1000):
s += str(i)
# 优
tmp = []
for i in range(1000):
tmp.append(str(i))
s = "".join(tmp)
六、资源与IO内存优化
- 文件、网络连接、数据库连接用完必关闭
优先with上下文管理器,自动释放资源
with open("test.txt","r",encoding="utf-8") as f:
text = f.readline()
# 出作用域自动关闭释放
- 日志、缓存设置上限,自动淘汰
缓存字典/列表加最大长度,超出自动丢弃旧数据,防止无限膨胀。
七、面向对象写法优化
- 类属性少存冗余数据
实例只存必要字段,冗余计算属性用@property动态计算,不常驻内存 - 慎用闭包长期捕获大对象
闭包会强引用外层大变量,导致无法GC回收 - 需要弱关联用 weakref 弱引用
不想阻碍垃圾回收,使用弱引用,不增加引用计数
八、GC 与内存回收优化
- 批量处理完数据后手动触发垃圾回收
import gc
gc.collect()
- 减少循环引用代码写法,从源头规避内存泄漏
九、第三方库内存技巧
- Pandas:
- 读取csv指定
usecols只读取需要列 - 调低数值类型
int64→int32、float64→float32
- 读取csv指定
- 图像处理:
处理完图像数组及时销毁,不要全局常驻大图矩阵
十、极简背诵总结(面试直接说)
- 多用生成器迭代器替代列表,分批加载大数据;
- 优先局部变量,严控全局大对象常驻内存;
- 资源使用
with自动释放,用完及时置空删除对象; - 字符串拼接用列表join,避免频繁新建字符串;
- 精简类实例属性,减少冗余存储,必要时使用弱引用;
- 大数据分页分块处理,杜绝一次性全量载入内存。
内存友好代码 VS 耗内存代码 对照表
一、大数据遍历
| 耗内存写法 | 内存友好写法 | 优势 |
|---|---|---|
data = list(range(10000000)) | data = range(10000000) | 不生成完整列表,迭代取值几乎无内存占用 |
列表推导式[x*2 for x in arr] | 生成器表达式(x*2 for x in arr) | 边遍历边计算,不存全部结果 |
二、变量作用域
| 耗内存 | 友好写法 |
|---|---|
| 全局定义大列表/大字典 | 放到函数内作为局部变量 |
| 长期保留无用大对象 | del obj / obj = None |
三、循环遍历
| 低效写法 | 优化写法 |
|---|---|
for i in arr[:]: 切片复制 | for i in arr: 直接遍历 |
| 循环内频繁创建空列表 | 外部提前创建,循环内清空复用 |
四、字符串拼接
| 浪费内存 | 高效写法 |
|---|---|
s += str(i) 循环累加 | 列表收集 + "".join() |
五、文件读取
| 耗内存 | 友好写法 |
|---|---|
f.read() 一次性读整文件 | for line in f: 逐行读取 |
| 不手动关闭文件 | with open() as f: 上下文管理器 |
六、容器存储
| 笨重写法 | 轻量化写法 |
|---|---|
| 多层嵌套字典存结构化数据 | namedtuple / dataclass |
| 纯Python列表存海量数值 | numpy.array 统一存储 |
| 列表无限追加不限制 | 设置最大长度,自动清理旧数据 |
七、数据处理
| 错误写法 | 正确写法 |
|---|---|
| 一次性读取全量数据库数据 | 分页查询、分批拉取 |
| 图像处理后全局保存原图 | 用完立即销毁图像数组 |
八、引用优化
| 容易泄漏 | 优化方案 |
|---|---|
| 闭包捕获超大外层变量 | 执行完毕手动解绑变量 |
| 大量对象强引用绑定 | weakref 弱引用 |
九、垃圾回收
| 放任不管 | 主动优化 |
|---|---|
| 依赖系统自动GC | 大批量数据处理后 gc.collect() |
| 大量循环引用代码 | 重构逻辑,减少互相引用 |
十、Pandas专属优化
| 高内存 | 省内存 |
|---|---|
| 读取所有列 | usecols 指定只读取需要列 |
| 默认int64/float64 | 向下转型 int32、float32 |
通用极简守则
- 能迭代不存全量,能局部不全局
- 能复用不新建,能清空不堆积
- 资源用完必释放,大对象用完必解绑
- 分批处理大数据,拒绝一次性塞满内存
更多推荐


所有评论(0)