Подтвердить что ты не робот

Коллекция 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

4b9b3361

Ответ 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/...)

Ответ 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) размер кучи обрезается. И в этот конкретный момент мое приложение получает огромный удар производительности - оно может повесить до секунд севета. Интересно, почему...