在 mvc 中使用后列表没有得到垃圾收集的问题

Issues with list not getting garbage collected after use in mvc

背景故事:我正在生成 csv 文件作为报告,并且正在测试如果生成多个大报告会发生什么,下面这个应该生成大约 4 MB 的 csv 文件,所以如果我调用 2 次来获取此报告我的电脑会限制我的 16 GB 内存,即使在获取文件后程序仍然使用我的所有内存,只有重新启动程序才能清理我的内存。 ram 主要用于 SMS 的样板 class。

我的问题是垃圾收集器永远不会 cleared/cleaned 收集 tmp 列表,即使在控制器调用完成后,也会导致大量 ram 被使用。

我可以看到 ram 使用仅在生成 tmp 列表时增加,而不是在创建 csv 文件时增加

控制台输出

Visual studio 点击下载一次后内存诊断

内存快照

private static readonly Random random = new();
private string GenerateString(int length = 30)
{
    StringBuilder str_build = new StringBuilder();
    Random random = new Random();

    char letter;

    for (int i = 0; i < length; i++)
    {
        double flt = random.NextDouble();
        int shift = Convert.ToInt32(Math.Floor(25 * flt));
        letter = Convert.ToChar(shift + 65);
        str_build.Append(letter);
    }
    return str_build.ToString();
}

[HttpGet("SMS")]
public async Task<ActionResult> GetSMSExport([FromQuery]string phoneNumber)
{
    Console.WriteLine($"Generating items: {DateTime.Now.ToLongTimeString()}");
    Console.WriteLine($"Finished generating items: {DateTime.Now.ToLongTimeString()}");
    Console.WriteLine($"Generating CSV: {DateTime.Now.ToLongTimeString()}");
    var tmp = new List<SMS>();
    // tmp never gets cleared by the garbage collector, even if its not used after the call is finished
    for (int i = 0; i < 1000000; i++)
    {
        tmp.Add(new SMS() { 
           GatewayName = GenerateString(),
           Message = GenerateString(),
           Status = GenerateString()
        });
    }
    // 1048574 is max, excel says 1048576 is max but because of header and seperater line it needs to be minussed with 2
    ActionResult csv = await ExportDataAsCSV(tmp, $"SMS_Report.csv");
    Console.WriteLine($"Finished generating CSV: {DateTime.Now.ToLongTimeString()}");
    return csv;
}
private async Task<ActionResult> ExportDataAsCSV(IEnumerable<object> listToExport, string fileName)
{
    Console.WriteLine("Creating file");
    if (listToExport is null || !listToExport.Any())
        throw new ArgumentNullException(nameof(listToExport));

    System.IO.File.Delete("Reports/" + GenerateString() + fileName);
    var file = System.IO.File.Create("Reports/" + GenerateString() + fileName, 4096, FileOptions.DeleteOnClose);

    var streamWriter = new StreamWriter(file, Encoding.UTF8);


    await streamWriter.WriteAsync("sep=;");
    await streamWriter.WriteAsync(Environment.NewLine);

    var headerNames = listToExport.First().GetType().GetProperties();
    foreach (var header in headerNames)
    {
        var displayAttribute = header.GetCustomAttributes(typeof(System.ComponentModel.DataAnnotations.DisplayAttribute),true);
        if (displayAttribute.Length != 0)
        {
            var attribute = displayAttribute.Single() as System.ComponentModel.DataAnnotations.DisplayAttribute;
            await streamWriter.WriteAsync(sharedLocalizer[attribute.Name] + ";");
        }
        else
            await streamWriter.WriteAsync(header.Name + ";");
    }
    await streamWriter.WriteAsync(Environment.NewLine);

    var newListToExport = listToExport.ToArray();

    for (int j = 0; j < newListToExport.Length; j++)
    {
        object item = newListToExport[j];
        var itemProperties = item.GetType().GetProperties();
        for (int i = 0; i < itemProperties.Length; i++)
        {
            await streamWriter.WriteAsync(itemProperties[i].GetValue(item)?.ToString() + ";");
        }
        await streamWriter.WriteAsync(Environment.NewLine);
    }
    Helpers.LogHelper.Log(Helpers.LogHelper.LogType.Info, GetType(), $"User {User.Identity.Name} downloaded {fileName}");
    await file.FlushAsync();
    file.Position = 0;
    
    return File(file, "text/csv", fileName);
}
public class SMS
{
    

    [Display(Name = "Sent_at_text")]
    public DateTime? SentAtUtc { get; set; }

    [Display(Name = "gateway_name")]
    public string GatewayName { get; set; }

    [Display(Name = "message_title")]
    [JsonProperty("messageText")]
    public string Message { get; set; }

    [Display(Name = "status_title")]
    [JsonProperty("statusText")]
    public string Status { get; set; }
}

答案可以在这个post中找到

已复制答案

github版本是这个问题的修复版本,所以你可以去探索我在变更集中所做的事情

备注:

  1. 生成大文件后,您可能需要在 C# 释放内存之前下载较小的文件
  2. 添加强制垃圾收集帮助很大
  3. 添加一些 using 语句也有很大帮助

较小的现有问题

  1. 如果您的对象无法放入 RAM 并且它开始填充页面文件,它不会在使用后减少页面文件(重新启动您的电脑会有所帮助,但不会完全清除页面文件)
  2. 无论我尝试什么,我都无法将它的 ram 使用量降低到 400MB 以下,但是如果我有 5GB 或 1GB 的 RAM 也没关系,它仍然会减少到 ~400MB