c# - 除了反射之外,还有什么方法可以解决 WPF 对 GC.Collect(2) 的调用?

标签 c# wpf reflection garbage-collection

我最近不得不将这个 怪物 checkin 生产代码以操作 WPF 类中的私有(private)字段:(tl;dr 我如何避免必须这样做?)

private static class MemoryPressurePatcher
{
    private static Timer gcResetTimer;
    private static Stopwatch collectionTimer;
    private static Stopwatch allocationTimer;
    private static object lockObject;

    public static void Patch()
    {
        Type memoryPressureType = typeof(Duration).Assembly.GetType("MS.Internal.MemoryPressure");
        if (memoryPressureType != null)
        {
            collectionTimer = memoryPressureType.GetField("_collectionTimer", BindingFlags.Static | BindingFlags.NonPublic)?.GetValue(null) as Stopwatch;
            allocationTimer = memoryPressureType.GetField("_allocationTimer", BindingFlags.Static | BindingFlags.NonPublic)?.GetValue(null) as Stopwatch;
            lockObject = memoryPressureType.GetField("lockObj", BindingFlags.Static | BindingFlags.NonPublic)?.GetValue(null);

            if (collectionTimer != null && allocationTimer != null && lockObject != null)
            {
                gcResetTimer = new Timer(ResetTimer);
                gcResetTimer.Change(TimeSpan.Zero, TimeSpan.FromMilliseconds(500));
            }
        }                
    }       

    private static void ResetTimer(object o)
    {
        lock (lockObject)
        {
            collectionTimer.Reset();
            allocationTimer.Reset();
        }
    }
}

要理解为什么我会做如此疯狂的事情,您需要查看 MS.Internal.MemoryPressure.ProcessAdd() :
/// <summary>
/// Check the timers and decide if enough time has elapsed to
/// force a collection
/// </summary>
private static void ProcessAdd()
{
    bool shouldCollect = false;

    if (_totalMemory >= INITIAL_THRESHOLD)
    {
        // need to synchronize access to the timers, both for the integrity
        // of the elapsed time and to ensure they are reset and started
        // properly
        lock (lockObj)
        {
            // if it's been long enough since the last allocation
            // or too long since the last forced collection, collect
            if (_allocationTimer.ElapsedMilliseconds >= INTER_ALLOCATION_THRESHOLD
                || (_collectionTimer.ElapsedMilliseconds > MAX_TIME_BETWEEN_COLLECTIONS))
            {
                _collectionTimer.Reset();
                _collectionTimer.Start();

                shouldCollect = true;
            }
            _allocationTimer.Reset();
            _allocationTimer.Start();
        }

        // now that we're out of the lock do the collection
        if (shouldCollect)
        {
            Collect();
        }
    }

    return;
}

重要的一点是接近尾声,它调用方法 Collect() :
private static void Collect()
{
    // for now only force Gen 2 GCs to ensure we clean up memory
    // These will be forced infrequently and the memory we're tracking
    // is very long lived so it's ok
    GC.Collect(2);
}

是的,这实际上是 WPF 强制执行第 2 代垃圾收集,从而强制执行完全阻塞的 GC。 A naturally occurring GC 发生时不会阻塞在第 2 代堆上。这在实践中意味着每当调用此方法时,我们的整个应用程序都会锁定。您的应用程序使用的内存越多,第 2 代堆的碎片越多,所需的时间就越长。我们的应用程序目前缓存了相当多的数据,很容易占用大量内存,强制 GC 可以将我们的应用程序锁定在慢速设备上几秒钟——每 850 毫秒。

因为尽管作者提出了相反的抗议,但很容易得出这种方法被频繁调用的场景。当从文件加载 BitmapSource 时,会出现 WPF 的此内存代码。我们 virtualize a listview 有数千个项目,其中每个项目都由存储在磁盘上的缩略图表示。当我们向下滚动时,我们正在动态加载这些缩略图,并且 GC 以最大频率发生。因此,随着应用程序不断锁定,滚动变得令人难以置信的缓慢和断断续续。

通过我在上面提到的那个可怕的反射 hack,我们强制永远不会满足计时器,因此 WPF 从不强制 GC。此外,似乎没有不利的后果——内存随着滚动而增长,最终自然触发 GC 而不锁定主线程。

是否有任何其他选项可以阻止对 GC.Collect(2) 的调用不像我的解决方案那么公然可怕?很想得到一个解释,说明跟进这个黑客可能会出现什么具体问题。我的意思是避免调用 GC.Collect(2) 的问题。 (在我看来,自然发生的 GC 应该就足够了)

最佳答案

Notice: Do this only if it causes a bottleneck in your app, and make sure you understand the consequences - See Hans's answer for a good explanation on why they put this in WPF in the first place.



你有一些讨厌的代码试图修复框架中的一个讨厌的黑客......因为它都是静态的并且从 WPF 中的多个地方调用,你真的不能比使用反射来破坏它做得更好(其他解决方案是 much worse ) .

所以不要指望那里有一个干净的解决方案。除非他们更改 WPF 代码,否则不存在这样的事情。

但我认为你的黑客可以更简单,避免使用计时器:只需破解 _totalMemory 值,你就完成了。它是 long ,这意味着它可以变为负值。并且非常大的负值。
private static class MemoryPressurePatcher
{
    public static void Patch()
    {
        var memoryPressureType = typeof(Duration).Assembly.GetType("MS.Internal.MemoryPressure");
        var totalMemoryField = memoryPressureType?.GetField("_totalMemory", BindingFlags.Static | BindingFlags.NonPublic);

        if (totalMemoryField?.FieldType != typeof(long))
            return;

        var currentValue = (long) totalMemoryField.GetValue(null);

        if (currentValue >= 0)
            totalMemoryField.SetValue(null, currentValue + long.MinValue);
    }
}

在这里,现在您的应用程序必须在调用 GC.Collect 之前分配大约 8 艾字节。不用说,如果发生这种情况,您将有更大的问题需要解决。 :)

如果您担心下溢的可能性,只需使用 long.MinValue / 2 作为偏移量。这仍然给您留下 4 艾字节。

请注意, AddToTotal 实际上执行 _totalMemory 的边界检查,但它使用 Debug.Assert here 执行此操作:
Debug.Assert(newValue >= 0);

由于您将使用 .NET Framework 的发布版本,这些断言将被禁用(使用 ConditionalAttribute ),因此无需担心。

您已经问过这种方法可能会出现什么问题。让我们来看看。
  • 最明显的一个:MS 更改了您试图破解的 WPF 代码。

    那么,在这种情况下,这在很大程度上取决于变化的性质。
  • 他们更改了类型名称/字段名称/字段类型:在这种情况下,将不会执行 hack,您将返回股票行为。反射代码非常具有防御性,它不会抛出异常,它不会做任何事情。
  • 他们将 Debug.Assert 调用更改为在发布版本中启用的运行时检查。在这种情况下,您的应用程序注定要失败。任何从磁盘加载图像的尝试都会失败。哎呀。

    由于他们自己的代码几乎是黑客行为,因此可以减轻这种风险。他们不打算扔它,它应该被忽视。他们希望它安静地坐着,默默地失败。让图像加载是一个更重要的特性,它不应该被一些内存管理代码削弱,这些代码的唯一目的是将内存使用量保持在最低限度。
  • 对于 OP 中的原始补丁,如果它们更改了常量值,则您的 hack 可能会停止工作。
  • 他们在保持类和字段完整的同时改变了算法。嗯……任何事情都有可能发生,这取决于变化。
  • 现在,让我们假设 hack 工作并成功禁用 GC.Collect 调用。

    在这种情况下,明显的风险是内存使用量增加。由于收集的频率较低,因此在给定时间将分配更多内存。这应该不是什么大问题,因为当第 0 代填满时,收集仍然会自然发生。

    您还会有更多的内存碎片,这是较少集合的直接结果。这对您来说可能是也可能不是问题 - 因此请配置您的应用程序。

    更少的集合也意味着更少的对象被提升到更高的代。这是一件好事。理想情况下,您应该在第 0 代中有短期对象,在第 2 代中有长期对象。频繁的收集实际上会导致短期对象被提升到第 1 代,然后被提升到第 2 代,你最终会得到在第 2 代中有许多无法访问的对象。这些对象只会被第 2 代收集清理,会导致堆碎片,并且实际上会增加 GC 时间,因为它必须花费更多时间来压缩堆。这实际上是为什么自己调用 GC.Collect 被认为是一种不好的做法的主要原因 - 您正在积极地击败 GC 策略,这会影响整个应用程序。

  • 在任何情况下,正确的方法是加载图像,缩小它们并在您的 UI 中显示这些缩略图。所有这些处理都应该在后台线程中完成。对于 JPEG 图像,加载嵌入的缩略图 - 它们可能已经足够了。并使用对象池,这样你就不需要每次都实例化新的位图,这完全绕过了 MemoryPressure 类问题。是的,这正是其他答案所暗示的;)

    关于c# - 除了反射之外,还有什么方法可以解决 WPF 对 GC.Collect(2) 的调用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36044796/

    相关文章:

    c# - 添加 log4net 配置时出现异常

    c# - ASP.NET Identity 有时无法登录

    WPF窗口的背景颜色不是由样式自动设置的

    c# - 将 ObservableCollection 绑定(bind)为不同 CollectionViewSource 的源

    c# - 是否可以使用 C# 在 SQLite 中获取列名(标题)?

    c# - 使用泛型时转换为抽象类或接口(interface)

    javascript - 在另一个 mailto 的正文中调用 Mailto

    c# - ArrayList 的.NET 并行处理

    xslt - 获取当前节点 xpath

    c# - 从字符串创建属性选择器表达式