Python 内存管理

一、一句话总结

Python 的内存管理就是三件事:

  1. 自动分配内存(你不用管变量存在哪)
  2. 自动回收垃圾(不用的对象自动删掉)
  3. 靠引用计数 + 分代垃圾回收实现

二、核心机制 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 不会每次都向操作系统要内存,而是自己维护一个内存池

  • 小块内存 → 从内存池拿
  • 用完归还内存池
  • 避免频繁申请/释放,速度快很多

六、完整流程(面试必背)

  1. 创建对象 → 自动分配内存
  2. 引用计数跟踪对象被使用次数
  3. 计数=0 → 立即释放
  4. 遇到循环引用GC分代回收清理
  5. 小内存使用内存池,提升效率

七、一句话总结

Python 内存管理以引用计数为基础,负责实时回收对象;
通过分代垃圾回收解决循环引用问题;
并使用内存池减少内存开销,提升效率。


Python 内存泄漏 + 排查方案(面试+实战)

一、先懂:什么是内存泄漏

本该被销毁的对象,引用一直没断开,GC回收不掉,内存只涨不跌
常见原因:

  1. 循环引用没断干净
  2. 全局变量一直持有大对象
  3. 长连接/缓存无限堆积不清空
  4. 线程、定时器、事件监听忘记关闭
  5. 第三方库/对象手动内存不释放

二、最常见 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 个实用方案

  1. 主动断开引用
    不用对象手动赋值 None
big_data = None
  1. 用完及时 del 删除
del a, b
  1. 清空全局缓存/列表
cache_list.clear()
  1. 避免闭包长期持有大对象
    用完主动解绑

  2. 手动触发GC
    定时执行

import gc
gc.collect()
  1. 改用弱引用 weakref
    不想强持有,用弱引用,不影响回收
import weakref
obj = weakref.proxy(原对象)

五、面试标准回答

Python内存泄漏大多由循环引用、全局常驻对象、闭包强引用、资源未释放导致;
排查可使用 gc 模块查看垃圾对象、psutil 监控进程内存、objgraph 追踪引用链、memory_profiler 逐行分析;
解决方式:主动置空引用、及时删除对象、定期清理缓存、使用弱引用、手动触发垃圾回收。


六、速记口诀

内存只涨不回落,大概率是引用锁;
循环引用全局存,闭包持有甩不脱;
gc查垃圾,psutil看涨跌,objgraph追源头。


如何写出内存友好的 Python 代码?

  1. 谨慎使用全局变量: 全局变量通常会进入 GC 的第 2 代,长期霸占内存。能放在函数内部的局部变量,就不要放在全局。
    避免死结般的循环引用: 如果在设计树状结构或图结构时不可避免地遇到双向引用(比如父节点指向子节点,子节点又指向父节点),请使用标准库中的 weakref(弱引用)模块。弱引用不会增加对象的引用计数。
    处理海量数据时使用生成器: 如上一篇文章所说,生成器按需产出数据,不会一次性撑爆内存池。
    手动触发 GC(必要时): 在跑完一个极消耗内存的数据清洗脚本后,可以通过 import gc; gc.collect() 强制执行一次全量垃圾回收,清理内存碎片。

深入底层,才能在架构和性能优化时游刃有余。Python 绝不仅仅是简单的脚本语言,它的内部设计同样充满了权衡与智慧。

一、核心原则

少创建、晚创建、及时释放、按需加载、避免常驻


二、变量与对象优化

  1. 不用就置空 / del 销毁
# 用完大对象立刻解绑
large_data = None
del large_data
  1. 优先局部变量,少用全局变量
    全局变量生命周期贯穿整个程序,长期占内存;局部函数执行完自动释放。
  2. 避免无意识创建临时大对象
# 差:生成完整新列表
new_list = [x*2 for x in big_list]
# 好:用生成器,不占整块内存
gen = (x*2 for x in big_list)
  1. 小数据复用对象,减少频繁新建
    循环内不要反复创建大容器,提前初始化复用。

三、容器选型(最关键)

  1. 海量遍历优先用 生成器 / 迭代器
    列表一次性载入全量数据,生成器边用边生成,几乎不占内存
# 占内存
nums = list(range(10000000))
# 内存友好
nums = range(10000000)
  1. 大量键值存储:按需选结构
  • 普通字典够用就不用嵌套多层字典
  • 固定结构数据用 namedtuple / dataclass 更省内存
  • 数值矩阵优先 numpy 数组,比原生列表省超多内存
  1. 大列表及时清空
data_list.clear()

四、循环写法优化

  1. 避免循环内嵌套循环生成大中间数据
  2. 遍历优先 for-in 迭代,不要先切片复制
# 浪费:复制一份新列表
for item in arr[:]:
    pass
# 友好:直接遍历原数组
for item in arr:
    pass
  1. 分批处理大数据,不要一次性全读入
    文件、日志、数据库数据分页/分块读取,而非一次性加载到内存。

五、字符串优化

  1. 大量拼接不用 +=
    频繁字符串拼接不断新建对象,改用列表收集最后 join
# 差
s = ""
for i in range(1000):
    s += str(i)

# 优
tmp = []
for i in range(1000):
    tmp.append(str(i))
s = "".join(tmp)

六、资源与IO内存优化

  1. 文件、网络连接、数据库连接用完必关闭
    优先 with 上下文管理器,自动释放资源
with open("test.txt","r",encoding="utf-8") as f:
    text = f.readline()
# 出作用域自动关闭释放
  1. 日志、缓存设置上限,自动淘汰
    缓存字典/列表加最大长度,超出自动丢弃旧数据,防止无限膨胀。

七、面向对象写法优化

  1. 类属性少存冗余数据
    实例只存必要字段,冗余计算属性用@property动态计算,不常驻内存
  2. 慎用闭包长期捕获大对象
    闭包会强引用外层大变量,导致无法GC回收
  3. 需要弱关联用 weakref 弱引用
    不想阻碍垃圾回收,使用弱引用,不增加引用计数

八、GC 与内存回收优化

  1. 批量处理完数据后手动触发垃圾回收
import gc
gc.collect()
  1. 减少循环引用代码写法,从源头规避内存泄漏

九、第三方库内存技巧

  1. Pandas:
    • 读取csv指定usecols只读取需要列
    • 调低数值类型int64→int32float64→float32
  2. 图像处理:
    处理完图像数组及时销毁,不要全局常驻大图矩阵

十、极简背诵总结(面试直接说)

  1. 多用生成器迭代器替代列表,分批加载大数据;
  2. 优先局部变量,严控全局大对象常驻内存;
  3. 资源使用with自动释放,用完及时置空删除对象;
  4. 字符串拼接用列表join,避免频繁新建字符串;
  5. 精简类实例属性,减少冗余存储,必要时使用弱引用;
  6. 大数据分页分块处理,杜绝一次性全量载入内存。

内存友好代码 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

通用极简守则

  1. 能迭代不存全量,能局部不全局
  2. 能复用不新建,能清空不堆积
  3. 资源用完必释放,大对象用完必解绑
  4. 分批处理大数据,拒绝一次性塞满内存
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐