You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

长期运行的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()

问题排查思路

咱先一步步拆解为啥内存会涨:

  1. Matplotlib动画的隐式状态存储:虽然你设了cache_frame_data=False,但FuncAnimation在blit=False时,可能还是会在内部保留部分帧相关的引用;每次update返回的bars对象,也可能被动画实例悄悄记录下来,日积月累就占了内存。
  2. Pandas数据处理的残留引用:每次get_data()都要读整个5MB的CSV,还要做分组、聚合、apply等操作——Pandas在这些过程中会生成大量中间DataFrame/Series,尤其是用lambda函数时,它会捕获上下文环境,可能不小心把大对象的引用链保留住,导致垃圾回收器没法清理。
  3. 全局对象的累积状态:全局的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 07:29:38