Коллекция Gen2 не всегда собирает мертвые объекты?
Наблюдая за счетчиком производительности CLR #Bytes in all Heaps
нового приложения .NET 4.5 сервера за последние несколько дней, я могу заметить шаблон, который заставляет меня думать, что коллекция Gen2 не всегда собирает мертвые объекты, но у меня возникают проблемы понимая, что именно происходит.
Серверное приложение работает в .NET Framework 4.5.1 с использованием GC GC/Background.
Это консольное приложение, размещенное как служба Windows (с помощью рамок Topshelf)
Серверное приложение обрабатывает сообщения, а пропускная способность как-то довольно постоянна.
Что я вижу, глядя на график CLR #Bytes in all Heaps
, так это то, что память начала около 18 МБ, а затем увеличилась до 35 МБ примерно за 20-24 часа (с 20-30 коллекциями Gen2 в течение этого временного интервала), а затем все внезапное падение до номинального значения 18 МБ, а затем рост до ~ 35 МБ в течение 20-24 часов и падение до 18 МБ и т.д. (я вижу, что образец повторяется в течение последних 6 дней, когда приложение запущено)... Рост памяти не является линейным, требуется около 5 часов для роста на 10 МБ, а затем 15-17 часов для оставшихся 10 МБ или около того.
Дело в том, что я вижу, глядя на счетчики perfmon для #Gen0/#Gen1/#Gen2 collections
, что куча коллекций Gen2 происходит в течение 20-24 часов (может быть, около 30), и ни одна из них не возвращает память к номинальному 18MB.
Однако странным является использование внешнего инструмента для принудительного GC (Perfview в моем случае), тогда я вижу, что #Induced GC
растет на 1 (GC.Collect был вызван так, что это нормально), и сразу же идет память назад к номинальному 18 МБ.
Что заставляет меня думать, что либо счетчик perfmon для коллекций # Gen2 не прав, и только одна коллекция Gen2 происходит через 20-22 часа или около того (meeehhh я действительно так не думаю) или что коллекция Gen2 не всегда собирать мертвые объекты (кажется более правдоподобным)... но в этом случае почему бы заставить GC через GC.Collect сделать трюк, какая разница между явным вызовом в GC.Collect, а также автоматически сгенерированные коллекции во время жизни приложения.
Я уверен, что есть очень хорошее объяснение, но из другого источника документации, который я нашел о GC -too few: (- коллекция Gen2 собирает мертвые объекты в любом случае. Поэтому, возможно, документы не актуальны или Я неправильно понял... Любое объяснение приветствуется. Спасибо!
РЕДАКТИРОВАТЬ: Смотрите этот снимок экрана #Bytes in all heaps
в течение 4 дней
(Нажмите, чтобы увеличить)
![graph]()
это проще, чем пытаться графовать вещи в голове. То, что вы можете видеть на графике, - это то, что я сказал выше... память увеличилась за 20-24 часа (и 20-30 коллекций Gen2 в течение этого временного интервала), пока не достигнет ~ 35 МБ, а затем внезапно опустится. В конце графика вы заметите, что индуцированный GC я запускается через внешний инструмент, немедленно отбрасывая память на номинальную.
EDIT # 2: Я сделал много очистки в коде, в основном в отношении финализаторов. У меня было много классов, которые ссылались на одноразовые типы, поэтому мне пришлось реализовать IDisposable
для этих типов. Однако в некоторых случаях я вводил в заблуждение некоторые статьи о реализации шаблона Diposable с Finalizer. Прочитав некоторую документацию MSDN, я понял, что финализатор необходим только тогда, когда тип сам хранит собственные ресурсы (и в этом случае этого можно избежать с помощью SafeHandle). Поэтому я удалил все финализаторы из всех этих типов. В коде были некоторые другие модификации, но в основном бизнес-логика, ничего ".NET framework" не было связано.
Теперь график очень отличается, это плоская линия вокруг 20 МБ в течение нескольких дней... точно, что я ожидал увидеть!
Итак, проблема теперь исправлена, однако я до сих пор не знаю, в чем проблема из-за... Похоже, что это могло быть связано с финализаторами, но все же не объясняет, что я заметил, даже если мы не вызывали Dispose (true) - подавление финализатора - поток финализатора должен пинаться между коллекцией, а не каждые 20-24 часа?!
Учитывая, что мы сейчас отошли от проблемы, потребуется время, чтобы вернуться к "багги" и воспроизвести ее снова. Я могу попытаться сделать это некоторое время, хотя и пойти на дно.
EDIT: Добавлен граф коллекции Gen2 (Нажмите для увеличения)
![graph]()
Ответы
Ответ 1
Из
http://msdn.microsoft.com/en-us/library/ee787088%28v=VS.110%29.aspx#workstation_and_server_garbage_collection
Условия сбора мусора
Сбор мусора происходит, когда выполняется одно из следующих условий: верно:
-
Система имеет низкую физическую память.
-
Память, которая используется выделенными объектами в управляемой куче, превосходит допустимый порог. Этот порог непрерывно отрегулированный по ходу процесса.
-
Вызывается метод GC.Collect. Почти во всех случаях вам не нужно вызывать этот метод, потому что сборщик мусора работает непрерывно. Этот метод в основном используется для уникальных ситуаций и тестирование.
Кажется, что вы атакуете 2-й, а 35 - порог. Вы должны иметь возможность настроить порог на что-то другое, если 35 имеет большой размер.
В коллекциях gen2 нет ничего особенного, из-за чего они будут отклоняться от этих правил. (cf fooobar.com/questions/218804/...)
Ответ 2
Являются ли какие-либо из ваших объектов "большими" объектами? есть отдельная "куча больших объектов", которая имеет разные правила
Фрагментация кучи больших объектов
Было улучшено в 4.5.1, хотя:
http://blogs.msdn.com/b/dotnet/archive/2011/10/04/large-object-heap-improvements-in-net-4-5.aspx
Ответ 3
Это можно легко объяснить, если включена gcTrimCommitOnLowMemory
. Обычно GC хранит некоторую дополнительную память, выделенную для процесса. Однако, когда память достигает определенного порога, GC затем "обрезает" дополнительную память.
Из документов:
Когда параметр gcTrimCommitOnLowMemory включен, сборщик мусора оценивает загрузку системной памяти и переходит в режим обрезки, когда нагрузка достигает 90%. Он поддерживает режим обрезки до тех пор, пока нагрузка не упадет ниже 85%.
Это может легко объяснить ваш сценарий - резервные копии хранилища сохраняются (и используются) до тех пор, пока ваше приложение не достигнет определенной точки, которая, как представляется, раз в 20-24 часа, в этот момент обнаружена 90% -ная нагрузка и память обрезается до минимальных требований (18 мб).
Ответ 4
Считывая вашу первую версию, я бы сказал, что это нормальное поведение.
... но в этом случае зачем принудительно использовать GC через GC.Collect трюк, какая разница между явным вызовом в GC.Collect, v.s автоматически сгенерированные коллекции в течение всего срока службы приложения.
Существует два типа коллекций, полная коллекция и частичная коллекция. То, что автоматически срабатывает, является частичной коллекцией, но при вызове GC.Collect он будет выполнять полную коллекцию.
Между тем, у меня может быть причина этого сейчас, когда вы сказали нам, что используете финализатор для всех своих объектов. Если по какой-либо причине один из этих объектов был повышен до # 2 Gen, финализатор будет работать только при создании коллекции # 2 Gen.
Следующий пример продемонстрирует, что я только что сказал.
public class ClassWithFinalizer
{
~ClassWithFinalizer()
{
Console.WriteLine("hello from finalizer");
//do nothing
}
}
static void Main(string[] args)
{
ClassWithFinalizer a = new ClassWithFinalizer();
Console.WriteLine("Class a is on #{0} generation", GC.GetGeneration(a));
GC.Collect();
Console.WriteLine("Class a is on #{0} generation", GC.GetGeneration(a));
GC.Collect();
Console.WriteLine("Class a is on #{0} generation", GC.GetGeneration(a));
a = null;
Console.WriteLine("Collecting 0 Gen");
GC.Collect(0);
GC.WaitForPendingFinalizers();
Console.WriteLine("Collecting 0 and 1 Gen");
GC.Collect(1);
GC.WaitForPendingFinalizers();
Console.WriteLine("Collecting 0, 1 and 2 Gen");
GC.Collect(2);
GC.WaitForPendingFinalizers();
Console.Read();
}
Выход будет:
Class a is on #0 generation
Class a is on #1 generation
Class a is on #2 generation
Collecting 0 Gen
Collecting 0 and 1 Gen
Collecting 0, 1 and 2 Gen
hello from finalizer
Как вы можете видеть, только при выполнении коллекции в генерации, где находится объект, будет восстановлена память объектов с финализатором.
Ответ 5
Просто подумайте, что я запишу 2 цента здесь. Я не эксперт в этом, но, возможно, это может помочь вашему исследованию.
Если вы используете 64-битную платформу, попробуйте добавить это в свой файл .config. Я читал, что это может быть проблемой.
<configuration>
<runtime>
<gcAllowVeryLargeObjects enabled="true" />
</runtime>
</configuration>
Единственное, что я хотел бы указать, это то, что вы можете доказать свою гипотезу путем устранения неполадок внутри, если вы контролируете исходный код.
Вызов чего-то в соответствии с этим классом, использующим основную память приложения, и его запуск с интервалами времени может пролить свет на то, что происходит на самом деле.
private void LogGCState() {
int gen = GC.GetGeneration(this);
//------------------------------------------
// Comment out the GC.GetTotalMemory(true) line to see what happening
// without any interference
//------------------------------------------
StringBuilder sb = new StringBuilder();
sb.Append(DateTime.Now.ToString("G")).Append('\t');
sb.Append("MaxGens: ").Append(GC.MaxGeneration).Append('\t');
sb.Append("CurGen: ").Append(gen).Append('\t');
sb.Append("CurGenCount: ").Append(GC.CollectionCount(gen)).Append('\t');
sb.Append("TotalMemory: ").Append(GC.GetTotalMemory(false)).Append('\t');
sb.Append("AfterCollect: ").Append(GC.GetTotalMemory(true)).Append("\r\n");
File.AppendAllText(@"C:\GCLog.txt", sb.ToString());
}
Кроме того, здесь используется довольно хорошая статья при использовании метода GC.RegisterForFullGCNotification
. Очевидно, это позволит вам также включить временной диапазон полной коллекции, чтобы вы могли настраивать производительность и частоту сбора данных для ваших конкретных потребностей. Этот метод также позволяет указать порог кучи для запуска уведомлений (или коллекций?).
Также есть возможность установить это в файле .config приложений, но я не смотрел. В большинстве случаев 35MB является довольно небольшим размером для серверного приложения в наши дни. Черт, мой веб-браузер иногда делает это до 300-400 МБ:) Итак, Framework может просто увидеть 35 МБ как хорошую точку по умолчанию, чтобы освободить память.
Во всяком случае, я могу сказать по задумчивости вашего вопроса, что я, вероятно, просто укажу очевидное. Но, это стоит упомянуть. Желаю вам удачи!
На забавную заметку
В начале этого сообщения я изначально написал "if (вы используете 64-битную платформу)". Это заставило меня взломать. Будьте осторожны!
Ответ 6
У меня точно такая же ситуация в моем приложении WPF. Нет финализаторов в моем коде кстати. Однако кажется, что текущий GC фактически собирает объекты Gen 2. Я вижу, что результаты GC.GetTotalMemory() уменьшаются до 150 мб после запуска коллекции Gen2.
Итак, у меня сложилось впечатление, что размер кучи Gen2 не показывает количество байтов, которое используется живыми объектами. Это скорее просто размер кучи или количество байтов, выделенных для целей Gen2. У вас может быть много свободной памяти.
В некоторых условиях (не для каждой коллекции gen 2) размер кучи обрезается. И в этот конкретный момент мое приложение получает огромный удар производительности - оно может повесить до секунд севета. Интересно, почему...