解决C# Web应用内存缓存引发的System.OutOfMemoryException问题
内存溢出异常排查与处理方案
一、问题根源分析
1. 线程启动场景的内存问题
- 线程无限制创建:每次调用
PreloadSavedCartsFor都会新建Thread实例,高并发下频繁调用会导致大量线程堆积,线程本身的栈内存、上下文资源长期占用无法回收。 - HttpContext引用泄漏:将当前
HttpContext赋值给后台线程的Current属性,线程执行完毕后未清理该引用。HttpContext包含Session、Request等大量资源,会导致GC无法正常回收这些对象,引发内存泄漏。 - 异常吞噬隐藏问题:空catch块掩盖了线程执行过程中的所有异常,无法及时发现缓存加载失败、资源释放异常等问题,导致隐患长期积累。
2. 图片读取场景的内存问题
- Image.FromFile的隐性内存泄漏:虽然用
using释放了Image对象,但Image.FromFile会锁定文件句柄并保留内部非托管内存缓存,短时间内大量处理大尺寸图片时,非托管内存无法及时回收。 - 内存重复占用:把原图片存入
MemoryStream再转成byte[],这部分内存要等缩略图生成后才释放,高并发处理图片时会导致托管内存瞬间飙升。 - 缓存机制失效:调用
RetrieveImage时传入checkCacheExpiration=false,跳过缓存检查,每次都从文件读取原图片,重复加载相同图片会造成内存重复占用,浪费资源。
二、修复与优化方案
1. 线程启动场景修复
- 改用线程池管理线程:用
Task.Run替代手动创建线程,线程池会自动处理线程的创建与回收,避免线程堆积:public void PreloadSavedCartsFor(LoggedInUser loggedinUser) { var sessionId = System.Web.HttpContext.Current?.Session?.SessionID; Task.Run(() => { try { var savedCarts = LoadCustomerSavedCarts(loggedinUser); if (!string.IsNullOrEmpty(sessionId)) { var currentSession = System.Web.HttpContext.Current?.Session; if (currentSession != null && currentSession.SessionID == sessionId) { currentSession["SavedCarts"] = savedCarts; } } } catch (Exception ex) { // 记录详细异常日志,包括用户ID、时间、堆栈信息 Logger.Error($"预加载用户{loggedinUser.Id}购物车失败:{ex.Message}", ex); } }); } - 移除跨线程HttpContext引用:避免直接传递
HttpContext.Current,改用SessionID或用户标识获取数据,切断资源泄漏的源头。 - 完善异常日志:删除空catch块,记录异常详情,便于定位线程执行中的问题。
2. 图片读取场景修复
- 优化图片加载方式:用
FileStream替代Image.FromFile,手动控制流的释放,减少非托管内存占用:public override Image RetrieveImage(string name, bool checkCacheExpiration = true) { lock (_Sync) { if (checkCacheExpiration ? Exists(name) : this.FileExists(name)) { var path = ResolvePath(name); using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read)) { return Image.FromStream(fs); } } else { return null; } } } - 减少内存中间转换:直接使用
Image对象生成缩略图,跳过MemoryStream转byte[]的步骤,降低内存占用:private static void CreateResizedImageFromOriginalImage(string ImageName, string ImageDimension, string newsizename, ref string returnName, ref string contentType) { using (System.Drawing.Image originalImage = ImageService.RetrieveImage(ImageName, true)) // 启用缓存检查 { if (originalImage != null) { contentType = ImageService.RetriveImageContentType(ImageName); byte[] newdata = null; var dims = ImageDimension.Split('X'); int imagewidth = dims.Length >= 1 && !string.IsNullOrEmpty(dims[0]) ? Convert.ToInt32(dims[0], System.Globalization.CultureInfo.InvariantCulture) : 0; int imageheight = dims.Length >= 2 && !string.IsNullOrEmpty(dims[1]) ? Convert.ToInt32(dims[1], System.Globalization.CultureInfo.InvariantCulture) : 0; // 修改ImageGenerator支持直接传入Image对象,避免内存转换 newdata = ImageGenerator.CreateImageThumbnail(originalImage, contentType, imageheight, imagewidth, false); if (newdata != null && newdata.Length > 0) { ImageService.SaveImage(newsizename, newdata, contentType); returnName = ImageService.RetrieveImageUrl(newsizename); } } } } - 启用缓存机制:调用
RetrieveImage时使用默认的checkCacheExpiration=true,利用缓存减少重复加载,降低内存和IO消耗。
3. 通用内存监控与预防
- 内存快照分析:用Visual Studio内存分析器或dotMemory定期抓取内存快照,分析托管堆中的对象分布,定位未释放的
HttpContext、Image对象或缓存条目。 - 配置缓存过期策略:若用
MemoryCache存储数据,设置合理的过期时间,避免缓存永久占用内存:var cache = MemoryCache.Default; var policy = new CacheItemPolicy { SlidingExpiration = TimeSpan.FromHours(2), // 2小时无访问自动过期 Priority = CacheItemPriority.Default }; cache.Set($"SavedCarts_{loggedinUser.Id}", savedCarts, policy); - 应用池定期回收:在IIS中设置应用池每天凌晨自动回收,作为兜底方案避免内存长期累积。
三、异常处理策略
- 全局异常捕获:在Global.asax中注册
Application_Error事件,捕获未处理的异常,记录日志并返回友好页面,避免应用崩溃:protected void Application_Error(object sender, EventArgs e) { var exception = Server.GetLastError(); Logger.Fatal($"未处理全局异常:{exception.Message}", exception); Server.ClearError(); Response.Redirect("/Error/ServerError"); } - 内存不足降级处理:在关键代码块中捕获
OutOfMemoryException,执行降级逻辑(如返回默认图片、跳过非关键缓存加载):try { // 图片处理或缓存加载代码 } catch (OutOfMemoryException ex) { Logger.Error("内存不足,执行降级逻辑", ex); returnName = "/Images/DefaultProduct.jpg"; }
内容的提问来源于stack exchange,提问作者Sandy
相关产品推荐
相关产品推荐

