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

Windows Service执行后持续高内存占用问题求助

Windows Service 执行后内存高占用排查与解决

问题概述

用C#开发的Windows Service,通过CRON表达式设置每日上午10点自动运行,遍历办公点列表并发送预约提醒邮件,执行2-3分钟后进入休眠状态。但任务管理器显示服务完成当日任务后仍占用约1.5GB内存;重启服务内存占用清零,次日运行后再次出现高占用,未检测到明显内存泄漏。

可能原因与排查方向

1. CLR垃圾回收未触发

任务管理器显示的是已分配内存(私有工作集),而非实际正在使用的内存。CLR的GC会在内存压力较大时才触发Full GC,服务休眠期间内存压力小,已分配的内存可能未被回收,看起来像是高占用,但实际并未发生泄漏。

2. 重复IO操作导致内存累积

代码中每次循环都读取邮件模板文件(reminderF.html/reminderS.html),频繁的IO操作会产生大量临时字符串对象,单个对象虽小,但循环次数多时会累积内存,且这些对象可能暂时未被GC回收。

3. 依赖服务资源未正确释放

  • 若_officeInformation、_appointmentInformation等服务为单例生命周期,可能缓存了大量办公点、预约数据未释放;
  • 数据库上下文(如EF Core DbContext)若未在Scope内正确释放,可能持有大量实体对象;
  • 邮件发送服务(_emailService)可能未正确释放SMTP连接或客户端资源,导致连接池或对象累积。

4. 日志组件缓存未刷新

若使用的日志框架(如Serilog、NLog)开启了缓存或批量写入,大量日志事件可能暂存在内存中未被写入文件,导致内存占用。

解决方案与优化步骤

1. 优化邮件模板读取逻辑

将邮件模板读取移至循环外,避免重复IO和对象分配:

public async Task Execute(IJobExecutionContext context)
{
    try
    {
        using var scope = _provider.CreateScope();
        InjectServices(scope);

        Log.Information("Service start");
        // 提前读取模板,避免循环内重复读取
        var firstShiftTemplate = await File.ReadAllTextAsync(
            Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "EmailTemplates/reminderF.html"));
        var secondShiftTemplate = await File.ReadAllTextAsync(
            Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "EmailTemplates/reminderS.html"));

        var allOfficesFirstShift = _officeInformation.ReadAllDueFirstShift();
        var allOfficesSecondShift = _officeInformation.ReadAllDueSecondShift();

        foreach (var office in allOfficesFirstShift)
        {
            var appointments = _appointmentInformation.ReadDueAppointments(office.id);
            foreach (var app in appointments)
            {
                try
                {
                    var currentUser = _staffInformation.GetById(app.userId);
                    
                    // 直接使用预读取的模板
                    var body = firstShiftTemplate;

                    // 简化收件人处理,减少List分配
                    string toEmails = string.IsNullOrEmpty(app.userEmail) ? string.Empty : app.userEmail;
                    
                    var emailMessage = new EmailMessage
                    {
                        FromName = $"{currentUser.firstName} {currentUser.lastName}",
                        From = $"{office.OfficeName}",
                        ReplyTo = currentUser.userEmail,
                        Subject = "Reminder",
                        To = toEmails,
                        IsHtml = true,
                        Body = body,
                    };

                   await _emailService.SendEmail(emailMessage);
                }
                catch (Exception e)
                {
                    Log.Information($"Reminder failure. Error: {e.Message}");
                }
            }
        }

        // 处理第二班次循环,使用预读取的模板
        foreach (var office in allOfficesSecondShift)
        {
            var appointments = _appointmentInformation.ReadDueAppointments(office.id);
            foreach (var app in appointments)
            {
                try
                {
                    var currentUser = _staffInformation.GetById(app.userId);
                    var body = secondShiftTemplate;

                    string toEmails = string.IsNullOrEmpty(app.userEmail) ? string.Empty : app.userEmail;

                    var emailMessage = new EmailMessage
                    {
                        FromName = $"{currentUser.firstName} {currentUser.lastName}",
                        From = $"{office.OfficeName}",
                        ReplyTo = currentUser.userEmail,
                        Subject = "Reminder",
                        To = toEmails,
                        IsHtml = true,
                        Body = body,
                    };

                    await _emailService.SendEmail(emailMessage);
                }
                catch (Exception e)
                {
                    Log.Information($"Reminder failure. Error: {e.Message}");
                }
            }
        }
    }
    catch (Exception ex)
    {
        Log.Error(ex.Message, ex);
    }

    Log.Information("Reminder job end");
    // 可选:测试手动触发GC(仅用于验证,生产环境不推荐)
    // GC.Collect();
    // GC.WaitForPendingFinalizers();
}

2. 验证内存实际使用情况

使用Visual Studio诊断工具或dotMemory获取内存快照,查看:

  • 哪些对象占用了大量内存;
  • 是否有未被释放的单例服务缓存数据;
  • 数据库上下文是否持有大量实体对象。

3. 检查依赖服务生命周期

确保_officeInformation、_appointmentInformation等数据访问服务为Scoped生命周期,在using var scope内创建,执行完后自动释放;若使用EF Core,DbContext必须是Scoped或Transient,避免单例导致的内存累积。

4. 优化邮件发送服务

若_emailService内部使用SmtpClient,确保用using包裹客户端实例,及时释放资源:

public async Task SendEmail(EmailMessage message)
{
    using var client = new SmtpClient("smtp.example.com");
    // 配置客户端并发送邮件
    await client.SendMailAsync(...);
}

5. 调整日志框架配置

若使用Serilog,配置即时刷新日志,避免缓存:

Log.Logger = new LoggerConfiguration()
    .WriteTo.File("logs/log.txt", flushToDiskInterval: TimeSpan.FromSeconds(1)) // 缩短刷新间隔
    .CreateLogger();

总结

先通过内存快照确认内存占用的具体对象,再针对性优化代码中的重复操作、服务生命周期和资源释放逻辑。多数情况下,这种“假泄漏”是GC未触发或临时对象累积导致,而非真正的内存泄漏。

内容的提问来源于stack exchange,提问作者Filip5991

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 22:09:24