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

