长期运行的Python实时绘图程序内存占用持续无限增长问题排查及优化咨询
长期运行的Python实时绘图程序内存占用持续无限增长问题排查及优化咨询
我写了一个Python实时绘图程序,打算连续运行几个月,用来从频繁更新的CSV文件里读取数据并绘制柱状图。但运行几天后发现内存占用一直在涨,Windows任务管理器显示内存越用越多,用gc.collect()也不管用,这可愁坏了,毕竟要跑好几个月呢。
先贴一下我的核心代码(完整代码也附在后面):
import pandas import numpy as np import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation fig, ax = plt.subplots() x = np.arange(5) # x-axis: 5 bars bars = ax.bar(x, np.zeros_like(x)) ax.set_ylim(0, 100) ax.set_title("Real-Time Bar Chart") def get_data(): df = pandas.read_csv('data/ztest_data.csv', header=None) # 约5.39MB的CSV,3列:字符串、整数、整数 df.columns = ['unit', 'value', 'selection'] df = df.loc[df['selection'] <= 5] df = df.groupby(['selection']).agg(avg_value=('value','mean')).reset_index() df['avg_value']/= 100000000 return df def update(frame): df = get_data() new_heights = np.random.randint(0, 50, size=5) + np.array(df['avg_value']) for bar, height in zip(bars, new_heights): bar.set_height(height) return bars ani = FuncAnimation(fig, update, interval=1000*5, blit=False, cache_frame_data=False) plt.show()
完整代码:
import os import re import gc import glob import pandas import datetime import numpy as np import matplotlib.pyplot as mp import matplotlib.dates as md from matplotlib.animation import FuncAnimation DEBUG_SIMULATE_PLOT_UPDATE = False DEBUG_TOTAL_INTERVALS = 20 if DEBUG_SIMULATE_PLOT_UPDATE: REFRESH_EVERY = 2 else: REFRESH_EVERY = 5*60 YMIN, YMAX = (0.5, 22) AMOUNTS = [154093, 139803*1.05/1.1, 92139, 136400, 115000] mp.style.use('dark_background') active_j_nums = [1,2,3,4,5] jcols = {'J1':(1,0,0),'J2':(0,0.9,0),'J3':(0.9,0.9,0),'J4':(0,0,1),'J5':(0.3,0.3,0.3),'J6':(0.5,0,1),'J7':(1,0,1),'J8':(210/255,149/255,87/255)} def float_hours_to_hhmm(hours_float): hours = int(hours_float) minutes = int(round((hours_float - hours) * 60)) return f"{hours:02d}:{minutes:02d}" def get_data(ii): weekdays = ['Monday', 'Tuesday', 'Wendesday', 'Thursday', 'Friday'] datafile = 'ztest_data.csv' df = pandas.read_csv(datafile, header=None) ind = min(len(df), ii*len(df)//DEBUG_TOTAL_INTERVALS) if DEBUG_SIMULATE_PLOT_UPDATE: df = df.iloc[:ind] df = df.rename(columns={0:'name', 1:'time', 2:'selection'}) df = df.loc[df['name'] == 'HDMI_B08'] grouped = df.groupby((df['selection'] != df['selection'].shift()).cumsum(), as_index=False).agg( selection=('selection',min), mint=('time',min), maxt=('time',max), ) grouped['delta_secs'] = grouped['maxt'] - grouped['mint'] grouped['delta_mins'] = grouped['delta_secs']/60 x1 = grouped[['selection', 'mint']] x2 = grouped[['selection', 'maxt']] dff = pandas.concat([x1.rename(columns={'mint':'time'}), x2.rename(columns={'maxt':'time'})]).sort_values(['time', 'selection']) values = list(range(1,9)) for val in values: dff[f'J{val}'] = np.where(dff['selection']==val, 1, 0) def calc_cutoff(x, hr, mn): dt = datetime.datetime.fromtimestamp(x) day_start = datetime.datetime(dt.year, dt.month, dt.day, hr, mn, 0) return int(day_start.timestamp()) def calculate_valid_time(row): min_ba = min(row['maxt'], row['day_end']) max_ef = max(row['mint'], row['day_start']) return max(min_ba - max_ef, 0) grouped['day_start'] = grouped['mint'].apply(lambda ii: calc_cutoff(ii, 0, 10)) grouped['day_end'] = grouped['mint'].apply(lambda ii: calc_cutoff(ii, 11+12, 50)) grouped['valid_time_during_day'] = grouped.apply(calculate_valid_time, axis=1).astype(int) grouped['day'] = grouped['mint'].apply(lambda row: datetime.datetime.fromtimestamp(row).strftime('%A')) grouped['daysort'] = pandas.to_datetime(grouped['mint'].apply(lambda row: datetime.datetime.fromtimestamp(row).date())) for which in ['mint', 'maxt', 'day_start', 'day_end']: grouped[f'{which}_readable'] = grouped[which].apply(lambda row: datetime.datetime.fromtimestamp(row).strftime('%b %d, %Y, %I:%M:%S %p')) grouped['week_num'] = grouped['mint'].apply(lambda ii : ((vv:=datetime.datetime.fromtimestamp(ii).isocalendar())[0],vv[1])) agg_by_week = grouped.groupby(['week_num', 'selection'], as_index=False).agg( total_valid_hours = ('valid_time_during_day', lambda ii : np.sum(ii)/(60*60)), total_hours = ('delta_secs', lambda ii: np.sum(ii)/(60*60)) ) return agg_by_week, grouped, dff def update(frame): agg_by_week, grouped, dff = get_data(frame) # 原代码中此处为图表更新逻辑,核心是处理数据后修改绘图元素 return bars ani = FuncAnimation(fig, update, interval=1000*REFRESH_EVERY, blit=False, cache_frame_data=False) mp.show()
问题排查思路
咱先一步步拆解为啥内存会涨:
- Matplotlib动画的隐式状态存储:虽然你设了
cache_frame_data=False,但FuncAnimation在blit=False时,可能还是会在内部保留部分帧相关的引用;每次update返回的bars对象,也可能被动画实例悄悄记录下来,日积月累就占了内存。 - Pandas数据处理的残留引用:每次
get_data()都要读整个5MB的CSV,还要做分组、聚合、apply等操作——Pandas在这些过程中会生成大量中间DataFrame/Series,尤其是用lambda函数时,它会捕获上下文环境,可能不小心把大对象的引用链保留住,导致垃圾回收器没法清理。 - 全局对象的累积状态:全局的
bars、fig、ax对象,每次更新时的修改会不会产生旧状态的残留?比如Matplotlib的轴对象可能会记录历史绘图数据。
具体优化方案
1. 先从Matplotlib动画配置下手
- 试试把
blit=True:开启blit模式后,Matplotlib只会重绘变化的部分,减少内部状态存储。注意开启后update返回的对象必须是可blit的,你当前返回bars是没问题的。 - 不要返回多余对象:如果
blit=False,其实返回值不影响重绘,你可以试试返回空元组(),避免动画实例存储大量历史返回对象。 - 显式清理动画帧:可以在
update函数里手动调用ani.new_frame_seq()重置帧序列,不过这个要谨慎测试,避免动画异常。
2. 重构Pandas数据处理,砍内存根源
- 只读取CSV新增内容:每次读整个5MB文件太浪费了,你可以记录上次读取的行数,下次用
skiprows参数跳过已读内容,只读新增行。比如用一个全局变量last_row = 0,每次get_data()里:
这样每次处理的数据量骤减,中间对象也少很多。global last_row df = pandas.read_csv(datafile, header=None, skiprows=last_row) new_rows = len(df) last_row += new_rows - 显式删除中间对象+强制GC:在
get_data()最后,把不用的中间DataFrame都删掉,再调用GC:del df, grouped gc.collect() return agg_by_week, dff - 替换lambda为普通函数:lambda会捕获上下文,把
calc_cutoff、calculate_valid_time移到get_data()外面,用参数传递需要的值,避免捕获大对象引用。 - 指定数据类型减少内存:读CSV时显式指定
dtype,比如df = pandas.read_csv(datafile, header=None, dtype={0:str, 1:np.int64, 2:np.int8}),selection列用np.int8足够,比默认的int64省内存。
3. 手动强化内存管理
在update函数最后,显式删除所有临时对象,再强制回收:
def update(frame): agg_by_week, grouped, dff = get_data(frame) # 图表更新逻辑 del agg_by_week, grouped, dff gc.collect() return bars
4. 用工具定位泄漏点
如果上面的方法都不管用,就用工具揪出元凶:
- 用tracemalloc(Python自带):在程序开头加
import tracemalloc; tracemalloc.start(),然后在update里每隔10帧打印内存快照:
这样就能看到哪行代码分配的内存一直没释放。if frame % 10 == 0: snapshot = tracemalloc.take_snapshot() top_stats = snapshot.statistics('lineno') print("Top 5内存占用行:") for stat in top_stats[:5]: print(stat) - 用objgraph:安装后在
update里打印增长最快的对象:import objgraph if frame % 10 == 0: objgraph.show_growth(limit=5)
长期运行的兜底建议
如果实在找不到完美的泄漏点,就搞个简单的监控重启机制:比如用Windows任务计划,每天凌晨自动关闭程序再重启;或者写个小脚本,每隔24小时检查一次程序内存,超过阈值就重启它。虽然粗暴,但对于长期运行来说是个可行的妥协方案。
内容来源于stack exchange
相关产品推荐
相关产品推荐

