【问题标题】:C#: Releasing memory usageC#:释放内存使用
【发布时间】:2019-11-20 18:12:20
【问题描述】:

我需要为一个开箱即用的问题执行一个冗长的过程。所以我把这个过程分成了多个子过程。现在的问题是,如何在每个窗口执行之前释放内存。

用一个例子更容易解释。我们来看看这个伪代码。

1. Some earlier code to do other things
2. Do
3.     Raise a Task
4.     If raised-task > 1000
5.         Wait all raised task to finish
6.         Release Memory
7.     End If
8. Wile Something not relevant

有了这个想法,我开发了下一个方法,每次达到线程限制时都会执行:

List<Task> LTask();
//It's not relevant, but this list is populate like
//var task = Task.Run(() => something());
//LTask.Add(task);

private void waitForAll()
{
    //Break point 1
    Task.WhenAll(LTasks).Wait();

    LTasks.Clear();
    LTasks = null;
    GC.Collect();
    GC.WaitForPendingFinalizers();

    //Break point 2
    LTasks = new List<Task>();
}

我预计内存在某些值附近会保持不变(有一些变化)。我的意思是:

  1. 已达到线程限制
  2. BreakPoint 1 中使用 Visual Studio 诊断工具的内存使用快照 --> 100MB
  3. BreakPont 2 中使用 Visual Studio 诊断工具的内存使用快照 --> 100 MB。 第一个问题,为什么没有减少?所有线程都结束了,我强制垃圾收集器执行。

下次达到限制并执行此代码时,如果我再次快照,内存不断增加:200、300、...

这是一个诊断工具的捕获。每次到达断点 1 时拍摄奇数快照,并在断点 2 时拍摄偶数快照。

第二个问题,这将继续无限增加,直到抛出 Out of memory Exception?

最后一个问题,有什么办法可以解决问题,释放内存?

更新 1: 经过一些测试,多亏了 cmets,我开发了一个测试代码来深入研究它。必须涉及其他事情。请看下一段代码。内存继续无限增长。

private List<Task> LTasks = new List<Task>();
private void manageThreadholdLimit()
{
    waitForAll();
    realeaseMemory();
}

private void waitForAll()
{
    Task.WhenAll(LTasks).Wait(); 
    LTasks.Clear();
    LTasks = null;  
}
private void realeaseMemory()
{   
    GC.Collect();
    GC.WaitForPendingFinalizers();

    LTasks = new List<Task>();
}
public void Main(){
    int i = 0;

    while (true)
    {
        i++;

        var task = Task.Run(() => Thread.Sleep(100));
        LTasks.Add(task);

        //Si hemos alcanzado el máximo de paralelismo, esperamos la conclusión
        if (i % 1000 == 0) manageThreadholdLimit();

    }
}

【问题讨论】:

  • 可能,它保持对线程内对象的引用。可以发一下任务代码吗?对于第二个问题:是的,它会在内存不足异常时增加。
  • 很遗憾,我无法粘贴任务代码,这是一个繁重的过程,并且包含一些明智的信息。但是,所有引发的任务都执行相同的功能,不接收任何参数。因此,当它在 LTask.Clear(); LTask = null; 中被取消引用时,它在内部声明的所有对象也应该被取消引用。
  • 您能否在 TaskManager 或 Process Explorer(如果有)中查看您正在调试的这个进程?查找内存和线程数。也许尝试在发布模式下运行应用程序来执行此操作,因为垃圾收集器可能会在那里更快地收集对象。
  • 我个人遇到了这个问题,通过代码将事件附加到 Winforms 对象,并且在关闭表单之前不卸载它们。同样就个人而言,我使用内存分析器解决了问题(如果您愿意,可以免费试用memprofiler.com)。它告诉了什么它被什么阻止了。
  • 你是否在这段代码中使用了事件?

标签: c# memory-management


【解决方案1】:

GC 收集在调试时略有不同,请参阅:(John Skeet knows all) 所以我会在发布模式下运行它时分配日志以验证所有行为。

任何解决方案都非常依赖于您的实际代码以及是否有非托管资源被访问。

也就是说,我以前必须处理过这样的问题,并且我以前曾以两种不同的方式“解决”过它。

一个解决方案

在执行实际工作的类中,让计数器在构造函数中递增,在终结器中递减,并等待计数器低于定义的阈值,重要的是再次运行 collect 以收集最终对象。

在继续之前请仔细检查总内存消耗,否则最终可能会出现内存不足异常。

现在这实际上会稍微增加您的内存消耗。欲了解更多信息see

另一种解决方案

通过使用 GC.GetTotalMemory() 甚至更好的性能计数器,有一个循环等待内存消耗下降,然后等待它下降。

如果您的资源没有被收集,这最终可能根本不做任何工作。

【讨论】:

  • 感谢您的帮助。我尝试了这两种解决方案,我可以看到每个偶数快照的内存使用量是如何减少的。但是,总的来说,它仍在增长。请看一下更新。
  • 您如何删除所有工作代码(我们还没有看到),而只是循环一个大尺寸的 char 数组。然后,如果您的代码正确释放了内存,您已经确定在您的工作人员的某个地方您持有一个引用,因此 gc 不会收集。
  • 另外我会尝试让它耗尽内存。然后稍微降低阈值,然后你会看到 GC 是否真的会释放内存,这也会让你知道你的应用程序将如何表现接近内存限制。
  • 再次感谢您的回复。请查看更新 1。这是您可以运行的完整测试。你会看到内存在增加(非常缓慢,但不断)。在我这边,它从 11mb 开始,3 分钟后达到 20mb。我不知道为什么,但我提出的每项任务都会在内存中存储一​​个永久垃圾,GC 无法删除
【解决方案2】:

不保证GarbageCollector 在您取消引用对象后会立即运行。事实上,它可能不会。即使您想使用GC.Collect() 手动调用它以进行测试,您也不能真正保证它会立即运行。此外,经常调用 GC 也是有代价的。可以使用 RAM(至少在具有大量 RAM 的较新机器中......)。如果你的记忆力上升,你就会有问题,并在一段时间后保持清醒。这通常表明您还有其他内存问题。也许你有泄漏?

如果我们谈论的是免费解决方案,您可以使用ProcessExplorerCLR Profiler 来查找潜在的内存问题。 Here 是一篇关于如何做到这一点的文章。

注意事项:

  1. 尽快将对象的引用设置为null。你完成了清单吗?将其值设置为null
  2. 将处理大量数据的方法拆分为更小的方法 - 即使您将某些内容设置为 null,GC 也不会在方法退出之前运行。
  3. 检查您是否在需要的地方正确实施了终结器。
  4. 确保您没有泄漏:检查您正在使用的对象是否没有从工作方法外部引用。请特别注意事件处理程序和数据绑定。

另外,Task.WhenAll() 显然保留了对其所有“子”任务的引用。如果您在同一方法中立即手动调用 GC,我认为它可能无法触及该内存,因为方法本身仍在“引用”它。

Here 是一篇关于垃圾回收的 MSDN 文章。

【讨论】:

  • 感谢您的帮助。我将Task.WhenAll()GC.Collect() 拆分为不同的方法,并复习您所说的内容。然而,记忆还在成长。请看一下更新。
【解决方案3】:

感谢@ScottChamberlain 在Why Garbage Collector doesn't collect Tasks objects的回答,我想我找到了解释

我认为内存会增加,因为 TaskScheduler 是静态的,并且仍然存储任务的引用,尽管任务已完成。所以引用的数量是不断增加内存的,而不是每个任务本身的内存使用量。

我还没有弄清楚如何删除这些引用,但作为替代解决方案,我可以重构使用线程的代码。例如,此代码是我问题中“更新 1”的替代方法。并且内存保持稳定在 18mb。

private static List<Thread> LThreads = new List<Thread>();
private static void manageThreadholdLimit()
{
    waitForAll();
    realeaseMemory();
}

private static void waitForAll()
{
    LThreads.ForEach(x => x.Join());
    LThreads.ForEach(x => x = null);
    LThreads.Clear();
    LThreads = null;
}
private static void realeaseMemory()
{


    GC.Collect();
    GC.WaitForPendingFinalizers();

    LTasks = new List<Task>();
    LThreads = new List<Thread>();
}
public static void Main(string[] args)
{
    int i = 0;

    while (true)
    {
        i++;

        var t = new Thread(() => Thread.Sleep(100));
        LThreads.Add(t);
        t.Start();

        if (i % 1000 == 0) manageThreadholdLimit();

    }
}

【讨论】:

    猜你喜欢
    • 2011-11-16
    • 1970-01-01
    • 2021-06-15
    • 2020-09-10
    • 2011-12-27
    • 2014-09-20
    • 1970-01-01
    • 1970-01-01
    • 2011-01-18
    相关资源
    最近更新 更多