内存泄露问题的排查

May 5, 20251 minute

内存泄露问题的排查

前言

最近遇到了一个典型的内存泄露问题。决定记录一下这次排查的完整思路和经验。

问题现象

公司存在一个提供API服务的服务器,频繁出现异常崩溃的情况。具体表现为:

  • 服务运行一段时间后突然崩溃
  • 崩溃前没有明显的业务异常日志
  • 重启后能正常运行,但过段时间又会崩溃
  • 崩溃间隔时间不固定,有时几小时,有时一天

初步怀疑是内存相关问题。

初步排查

内存监控分析

首先通过服务器监控工具观察内存使用情况,发现了一个关键线索:

  • 服务启动时内存使用正常
  • 随着时间推移,内存使用量持续上升
  • 当调用某个特定接口时,内存会出现明显的飙升
  • 内存达到服务器上限后,服务就会崩溃

这个现象让我基本确定是内存泄露问题,而且很可能与那个特定的接口有关。

日志分析

仔细分析了应用日志,发现:

  • 问题接口的调用频率确实比较高
  • 每次调用后内存都有增长趋势
  • 但业务逻辑本身没有报错

看起来问题就出在这个接口的实现上。

本地调试

使用Visual Studio进行内存调试

由于是比较老旧的服务,已经很久没有人维护了,拉取了生产环境对应版本的源码,准备在本地重现问题。

1. 启用诊断工具

调试 -> 窗口 -> 显示诊断工具

在调试过程中,诊断工具会实时显示内存使用情况。

2. 内存使用情况分析

  • 在诊断工具中点击"内存使用情况"
  • 可以看到实时的内存变化曲线
  • 当内存出现异常增长时,可以拍摄内存快照进行分析

3. 内存快照对比

  • 在问题接口调用前后分别拍摄快照
  • 通过快照对比,可以清楚看到哪些对象占用了大量内存
  • 找到内存增长的具体原因

问题定位

通过Visual Studio的内存分析工具,很快就定位到了问题所在。

根本原因

经过仔细分析代码,发现问题出现在以下代码逻辑中:

// 有问题的代码示例
public class DataService
{
    private static Dictionary<int, List<DataModel>> _cache = new Dictionary<int, List<DataModel>>();

    public List<DataModel> GetDataByCategory(int categoryId)
    {
        if (!_cache.ContainsKey(categoryId))
        {
            var dataList = new List<DataModel>();

            // 错误:在循环中重复查询数据库
            var subCategories = GetSubCategories(categoryId);
            foreach (var subCategory in subCategories)
            {
                var items = DatabaseQuery($"SELECT * FROM Items WHERE CategoryId = {subCategory.Id}");
                dataList.AddRange(items);
            }

            // 将查询结果存入静态字典缓存
            _cache[categoryId] = dataList;
        }

        return _cache[categoryId];
    }
}

问题分析

  1. 循环查询问题:在循环中对每个子分类都执行数据库查询,效率极低
  2. 静态缓存滥用:使用静态字典存储查询结果,且没有清理机制
  3. 数据量差异:由于开发环境数据量较小,内存攀升并不明显,但生产环境数据量巨大,多次请求后内存飙升

解决方案

1. 优化数据库查询

public List<DataModel> GetDataByCategory(int categoryId)
{
    // 优化:使用单次查询替代循环查询
    var sql = @"
        SELECT i.*
        FROM Items i
        INNER JOIN SubCategories sc ON i.CategoryId = sc.Id
        WHERE sc.ParentCategoryId = @categoryId";

    return DatabaseQuery(sql, new { categoryId });
}

2. 实现合理的缓存策略

public class DataService
{
    private readonly IMemoryCache _cache;
    private readonly TimeSpan _cacheExpiration = TimeSpan.FromMinutes(30);

    public DataService(IMemoryCache cache)
    {
        _cache = cache;
    }

    public List<DataModel> GetDataByCategory(int categoryId)
    {
        var cacheKey = $"category_{categoryId}";

        if (!_cache.TryGetValue(cacheKey, out List<DataModel> cachedData))
        {
            cachedData = QueryDataFromDatabase(categoryId);

            _cache.Set(cacheKey, cachedData, _cacheExpiration);
        }

        return cachedData;
    }
}