Недавно мне пришлось проверить этот монстра в производственный код, чтобы манипулировать частными полями в классе WPF: (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 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 ok
GC.Collect(2);
}
Да, этот WPF фактически заставляет сборку мусора gen 2, которая заставляет полностью блокировать GC. Естественно встречающийся GC происходит без блокировки кучи gen 2. На практике это означает, что всякий раз, когда вызывается этот метод, наше приложение блокируется. Чем больше памяти используется вашим приложением, и чем фрагментирована ваша куча gen 2, тем дольше это займет. Наше приложение в настоящее время кэширует довольно много данных и может легко захватить концертную память, а принудительный GC может заблокировать наше приложение на медленном устройстве в течение нескольких секунд - каждые 850 MS.
Ибо, несмотря на возражения автора об обратном, легко прийти к сценарию, где этот метод вызывается с большой частотой. Этот код памяти WPF возникает при загрузке BitmapSource
из файла. Мы виртуализируем список просмотров с тысячами элементов, где каждый элемент представлен миниатюрами, хранящимися на диске. Когда мы прокручиваем вниз, мы динамически загружаем эти миниатюры и что GC происходит на максимальной частоте. Таким образом, прокрутка становится невероятно медленной и изменчивой, при этом приложение постоянно блокируется.
С этим ужасным взломом отражения, о котором я говорил выше, мы вынуждаем таймеров никогда не встречаться, и поэтому WPF никогда не заставляет GC. Кроме того, по-видимому, никаких неблагоприятных последствий не наблюдается - память растет, когда один свиток и, в конечном счете, GC запускается естественным образом, не блокируя основной поток.
Есть ли другой способ предотвратить эти вызовы на GC.Collect(2)
, которые не настолько грубо отвратительны, как мое решение? Хотелось бы получить объяснение того, какие конкретные проблемы могут возникнуть в результате этого взлома. Под этим я подразумеваю проблемы с тем, чтобы избежать вызова GC.Collect(2)
. (мне кажется, что GC, естественно, должен быть достаточным)