Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Сборщик мусора .NET (GC) делит объекты на небольшие и большие объекты. Если объект большой, некоторые из его атрибутов становятся более значительными, чем если объект мал. Например, сжатие ( то есть копирование в памяти в другом месте кучи) может быть дорогостоящим. Из-за этого сборщик мусора помещает большие объекты в кучу больших объектов (LOH). В этой статье описывается, что классифицирует объект как крупный объект, как собираются крупные объекты и как крупные объекты влияют на производительность.
Это важно
В этой статье рассматривается куча больших объектов в .NET Framework и .NET Core, работающих только в системах Windows. Это не относится к выполнению LOH в реализациях .NET на других платформах.
Как объект попадает в LOH
Если объект больше или равен 85 000 байтам размера, он считается большим объектом. Это число определяется настройкой производительности. Если запрос на выделение объектов составляет 85 000 или более байт, среда выполнения размещает его в куче больших объектов.
Чтобы понять, что это означает, полезно изучить некоторые основы сборщика мусора.
Сборщик мусора является поколенческим сборщиком. Он имеет три поколения: поколение 0, поколение 1 и поколение 2. Причина трех поколений заключается в том, что в хорошо настроенном приложении большинство объектов умирают в 0-м поколении. Например, в серверном приложении выделение, связанное с каждым запросом, должно умереть после завершения запроса. Запросы на распределение в процессе обработки попадут в первое поколение и завершатся там. По сути, gen1 действует как буфер между молодыми областями объектов и долгоживущими областями объектов.
Новые выделенные объекты формируют новое поколение объектов и неявно создают коллекции 0. Однако если они являются большими объектами, они располагаются в куче больших объектов (LOH), которую иногда называют поколением 3. Поколение 3 — это физическое поколение, которое логически собирается в составе поколения 2.
Крупные объекты относятся к поколению 2, так как они собираются только во время коллекции поколения 2. Когда собирается поколение, также собираются все его подпоколения. Например, когда происходит сборка 1-го поколения, собираются и первое, и нулевое поколения. Когда происходит сборка мусора второго поколения (Generation 2), собирается вся куча. По этой причине сборка GC поколения 2 также называется полной сборкой данных. Эта статья относится к поколению 2 GC вместо полной сборки, но термины взаимозаменяемы.
Поколения предоставляют логическое представление кучи GC. Физически объекты живут в сегментах управляемой кучи. Сегмент управляемой кучи — это блок памяти, который GC резервирует из ОС, вызывая функцию VirtualAlloc от имени управляемого кода. При загрузке среды CLR ГК выделяет два начальных сегмента кучи: один для небольших объектов (куча небольших объектов или SOH) и один для больших объектов (куча больших объектов или LOH).
Затем запросы на выделение удовлетворяются путем размещения управляемых объектов в этих сегментах управляемой кучи. Если объект меньше 85 000 байт, он помещается в сегмент для SOH; в противном случае он помещается в сегмент LOH. Сегменты фиксируются (в небольших блоках), так как на них выделяется больше и больше объектов. Для SOH объекты, которые выжили GC, повышаются до следующего поколения. Объекты, которые выжили в коллекции поколения 0, теперь считаются объектами поколения 1 и т. д. Тем не менее, объекты, которые пережили старшее поколение, по-прежнему считаются относящимися к самому старому поколению. Другими словами, выжившие из поколения 2 являются объектами поколения 2; и выжившие из LOH являются объектами LOH (которые собираются вместе с поколением 2).
Пользовательский код может выделяться только в поколении 0 (небольшие объекты) или LOH (большие объекты). Только GC может "выделить" объекты в поколении 1 (путем поощрения выживших из поколения 0) и поколения 2 (путем поощрения выживших из поколения 1).
При активации сборки мусора GC выполняет трассировку по динамическим объектам и сжимает их. Но потому что уплотнение дорого, GC подметает LOH; он делает свободный список из мертвых объектов, которые можно повторно использовать позже для удовлетворения запросов на выделение больших объектов. Смежные мертвые объекты объединяются в один свободный объект.
.NET Core и .NET Framework (начиная с .NET Framework 4.5.1) включают GCSettings.LargeObjectHeapCompactionMode свойство, позволяющее пользователям указывать, что loH должен быть компактирован во время следующей полной блокировки GC. И в будущем .NET может решить сжать LOH автоматически. Это означает, что если вы выделяете большие объекты и хотите убедиться, что они не перемещаются, их все равно следует закрепить.
Рис. 1 иллюстрирует сценарий, в котором после первого сборщика мусора поколения 0, когда Obj1
и Obj3
были удалены, GC создает поколение 1; затем оно создает поколение 2 после первого сборщика мусора поколения 1, когда Obj2
и Obj5
были удалены. Обратите внимание, что этот и следующие рисунки предназначены только для иллюстрации; они содержат очень мало объектов, чтобы лучше показать, что происходит в куче. На самом деле, многие другие объекты обычно участвуют в GC.
Рис. 1. Сборщики мусора поколения 0 и поколения 1.
На рисунке 2 показано, что после создания 2-го поколения, который видел, что Obj1
и Obj2
мертв, GC формирует непрерывное свободное пространство из памяти, которое использовалось для удовлетворения Obj1
Obj2
запроса Obj4
на выделение. Пространство после последнего объекта Obj3
до конца сегмента также можно использовать для удовлетворения запросов на выделение.
Рис. 2. После создания 2-го поколения GC
Если недостаточно свободного места для удовлетворения запросов на выделение памяти под большие объекты, GC сначала пытается получить больше сегментов из операционной системы (ОС). Если это не удается, он активирует поколение 2 GC в надежде освободить некоторое пространство.
Во время сборки мусора поколения 1 или поколения 2 сборщик мусора освобождает сегменты, у которых нет живых объектов, возвращая их обратно в ОС, вызвав функцию VirtualFree. Пространство после последнего живого объекта до конца сегмента будет освобождено (за исключением эфемерного сегмента, где живут объекты поколений gen0/gen1, где сборщик мусора сохраняет часть которых остается выделенной, поскольку ваше приложение будет сразу выделять память в этом сегменте). И свободные пространства остаются зафиксированными, хотя они сбрасываются, что означает, что ОС не должна записывать данные в них обратно на диск.
Так как loH собирается только во время поколения 2 GCs, сегмент LOH может быть освобожден только во время такой сборки. На рисунке 3 показан сценарий, в котором сборщик мусора освобождает один сегмент (сегмент 2) обратно в ОС и освобождает больше места на оставшихся сегментах. Если ему нужно использовать деактивированное пространство в конце сегмента для удовлетворения запросов на выделение больших объектов, то он снова выделяет эту память. (Описание фиксации/деактивации см. в документации по VirtualAlloc.)
Рис. 3. LoH после сборки 2-го поколения
Когда собирается большой объект?
Как правило, GC происходит при одном из следующих трех условий.
Выделение ресурсов превышает пороговое значение для поколения 0 или большого объекта.
Пороговое значение является свойством поколения. Порог для поколения устанавливается, когда сборщик мусора выделяет в него объекты. При превышении порогового значения в этом поколении активируется GC. При выделении небольших или больших объектов задействуются пороговые значения поколения 0 и LOH соответственно. Когда сборщик мусора выделяется в поколение 1 и 2, он потребляет пороговые значения. Эти пороговые значения динамически настраиваются при выполнении программы.
Это типичный случай; большинство GCs происходят из-за выделения в управляемой куче.
вызывается метод GC.Collect .
Если метод без параметров GC.Collect() вызывается или другая перегрузка GC.MaxGeneration передается в качестве аргумента, LOH собирается вместе с остальной частью управляемой кучи.
Система испытывает недостаток памяти.
Это происходит, когда сборщик мусора получает уведомление о высокой памяти из ОС. Если сборщик мусора считает, что выполнение сборки мусора 2-го поколения будет продуктивным, он активирует её.
Последствия для производительности LOH
Выделение в куче больших объектов влияет на производительность следующим образом.
Стоимость распределения.
Среда CLR гарантирует, что память для каждого нового объекта, который она выделяет, очищена. Это означает, что стоимость выделения большого объекта в основном состоит из процесса очистки памяти от предыдущих данных (если только оно не вызывает GC). Если для очистки одного байта требуется два цикла, для очистки наименьшего большого объекта требуется 170 000 циклов. Очистка памяти объекта размером 16 МБ на компьютере с частотой 2 ГГц занимает около 16 мс. Это довольно большая стоимость.
Стоимость сбора.
Так как loH и поколение 2 собираются вместе, если превышено пороговое значение, активируется коллекция поколения 2. Если коллекция поколения 2 активируется из-за loH, поколение 2 не обязательно будет гораздо меньше после сборки GC. Если данных о втором поколении немного, то влияние минимально. Но если поколение 2 большое, это может вызвать проблемы с производительностью, если активируются многие 2-го поколения. Если на временной основе выделено много больших объектов, и у вас есть большой SOH, вы можете тратить слишком много времени на выполнение GCs. Кроме того, стоимость выделения памяти может существенно увеличиваться, если вы продолжаете выделять и освобождать очень большие объекты.
Элементы массива с ссылочными типами.
Очень большие объекты в LOH обычно массивы, и очень редко встречается экземпляр объекта действительно большого размера. Если элементы массива богаты ссылками, это влечет за собой расходы, которых нет, если элементы не богаты ссылками. Если в элементе нет ссылок, сборщику мусора вообще не нужно проходить через массив. Например, если вы используете массив для хранения узлов в двоичном дереве, одним из способов его реализации является ссылка на правый и левый узел узла фактическими узлами:
class Node { Data d; Node left; Node right; }; Node[] binary_tr = new Node [num_nodes];
Если
num_nodes
велик, сборщику мусора необходимо обработать как минимум две ссылки на элемент. Альтернативный подход заключается в хранении индекса правых и левых узлов:class Node { Data d; uint left_index; uint right_index; } ;
Вместо того, чтобы ссылаться на данные
left.d
левого узла, вы ссылаетесь на него какbinary_tr[left_index].d
. А сборщик мусора не должен смотреть ссылки на левый и правый узел.
Из трех факторов первые два обычно являются более значительными, чем третий. Из-за этого рекомендуется выделить пул больших объектов, которые повторно используются вместо выделения временных объектов.
Сбор данных по производительности для LOH
Прежде чем собирать данные о производительности для определенной области, необходимо выполнить следующие действия:
- Найдено свидетельство того, что вам следует обратить внимание на эту область.
- Были исследованы все известные вам области, не обнаружив ничего, что могло бы объяснить наблюдаемую вами проблему производительности.
Дополнительные сведения об основах памяти и ЦП см. в блоге "Общие сведения о проблеме", прежде чем попытаться найти решение.
Для сбора данных о производительности LOH можно использовать следующие средства:
Счетчики производительности памяти .NET CLR
Счетчики производительности памяти .NET CLR обычно являются хорошим шагом в изучении проблем с производительностью (хотя мы рекомендуем использовать события ETW). Распространенный способ просмотра счетчиков производительности — с помощью монитора производительности (perfmon.exe). Нажмите кнопку "Добавить " (CTRL + A), чтобы добавить интересные счетчики для процессов, которые вам нужны. Вы можете сохранить данные счетчика производительности в файл журнала.
Следующие два счетчика в категории памяти .NET CLR относятся к LOH:
Коллекции 2-го поколения
Отображает количество операций создания 2-го поколения с момента начала процесса. Счетчик увеличивается в конце сборки мусора поколения 2, которая также называется полной сборкой мусора. Этот счетчик отображает последнее наблюдаемое значение.
Размер кучи больших объектов
Отображает текущий размер в байтах, включая свободное пространство, loH. Этот счетчик обновляется в конце сборки мусора, а не при каждом выделении.
Вы также можете программно запрашивать счетчики производительности с помощью класса PerformanceCounter. Для LOH укажите ".NET CLR Memory" в качестве CategoryName и "размер кучи больших объектов" в качестве CounterName.
PerformanceCounter performanceCounter = new()
{
CategoryName = ".NET CLR Memory",
CounterName = "Large Object Heap size",
InstanceName = "<instance_name>"
};
Console.WriteLine(performanceCounter.NextValue());
Обычно счетчики собираются программным способом в рамках обычного процесса тестирования. При обнаружении счетчиков со значениями, которые не являются обычными, используйте другие средства, чтобы получить более подробные данные, чтобы помочь в расследовании.
Замечание
Мы рекомендуем использовать события ETW вместо счетчиков производительности, так как ETW предоставляет гораздо более подробную информацию.
События ETW
Сборщик мусора предоставляет широкий набор событий ETW, чтобы понять, что делает куча и почему. В следующих записях блога показано, как собирать и понимать события GC с помощью ETW:
Чтобы определить чрезмерное поколение 2 ГК, вызванные временными распределениями loH, ознакомьтесь со столбцом "Причина триггера" для GCs. Для простого теста, который выделяет только временные большие объекты, можно собирать сведения о событиях ETW с помощью следующей команды PerfView :
perfview /GCCollectOnly /AcceptEULA /nogui collect
Результат выглядит примерно так:
Как видно, все сборщики мусора относятся ко второму поколению и активируются AllocLarge, что означает, что выделение большого объекта вызвало этот сборщик мусора. Мы знаем, что эти выделения являются временными, потому что уровень выживания LOH % в столбце указывает 1%.
Вы можете собирать дополнительные события ETW, которые покажут, кто выделил эти большие объекты. Следующая командная строка:
perfview /GCOnly /AcceptEULA /nogui collect
собирает событие AllocationTick, которое запускается примерно каждые 100 тысяч выделений. Другими словами, событие запускается при каждом выделении большого объекта. Затем можно просмотреть одно из представлений GC Heap Alloc, в котором показаны стек вызовов, выделившие большие объекты.
Как видно, это очень простой тест, который просто выделяет большие объекты из своего Main
метода.
Отладчик
Если у вас есть дамп памяти, и вам нужно посмотреть, какие объекты на самом деле находятся в LOH, можно использовать расширение SoS для отладчика, предоставленное .NET.
Замечание
Команды отладки, упомянутые в этом разделе, применимы к отладчикам Windows.
Ниже приведен пример выходных данных из анализа LOH.
0:003> .loadby sos mscorwks
0:003> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x013e35ec
sdgeneration 1 starts at 0x013e1b6c
generation 2 starts at 0x013e1000
ephemeral segment allocation context: none
segment begin allocated size
0018f2d0 790d5588 790f4b38 0x0001f5b0(128432)
013e0000 013e1000 013e35f8 0x000025f8(9720)
Large object heap starts at 0x023e1000
segment begin allocated size
023e0000 023e1000 033db630 0x00ffa630(16754224)
033e0000 033e1000 043cdf98 0x00fecf98(16699288)
043e0000 043e1000 05368b58 0x00f87b58(16284504)
Total Size 0x2f90cc8(49876168)
------------------------------
GC Heap Size 0x2f90cc8(49876168)
0:003> !dumpheap -stat 023e1000 033db630
total 133 objects
Statistics:
MT Count TotalSize Class Name
001521d0 66 2081792 Free
7912273c 63 6663696 System.Byte[]
7912254c 4 8008736 System.Object[]
Total 133 objects
Размер кучи loH составляет (16 754 224 + 16 699 288 + 16 284 504) = 49 738 016 байт. Между адресами 023e1000 и 033db630 массив объектов System.Object занимает 8 008 736 байт, массив объектов занимает 6 663 696 байт, а свободное пространство System.Byte занимает 2 081 792 байта.
Иногда отладчик показывает, что общий размер loH меньше 85 000 байт. Это происходит потому, что сама среда выполнения использует loH для выделения некоторых объектов, которые меньше большого объекта.
Поскольку loH не сжимается, иногда loH считается источником фрагментации. Фрагментация означает:
Фрагментация управляемой кучи, указываемая объемом свободного пространства между управляемыми объектами. В SoS
!dumpheap –type Free
команда отображает объем свободного пространства между управляемыми объектами.Фрагментация адресного пространства виртуальной памяти( виртуальной машины), которая является памятью, помеченной как
MEM_FREE
. Его можно получить с помощью различных команд отладчика в windbg.В следующем примере показана фрагментация в пространстве виртуальной машины:
0:000> !address 00000000 : 00000000 - 00010000 Type 00000000 Protect 00000001 PAGE_NOACCESS State 00010000 MEM_FREE Usage RegionUsageFree 00010000 : 00010000 - 00002000 Type 00020000 MEM_PRIVATE Protect 00000004 PAGE_READWRITE State 00001000 MEM_COMMIT Usage RegionUsageEnvironmentBlock 00012000 : 00012000 - 0000e000 Type 00000000 Protect 00000001 PAGE_NOACCESS State 00010000 MEM_FREE Usage RegionUsageFree … [omitted] -------------------- Usage SUMMARY -------------------------- TotSize ( KB) Pct(Tots) Pct(Busy) Usage 701000 ( 7172) : 00.34% 20.69% : RegionUsageIsVAD 7de15000 ( 2062420) : 98.35% 00.00% : RegionUsageFree 1452000 ( 20808) : 00.99% 60.02% : RegionUsageImage 300000 ( 3072) : 00.15% 08.86% : RegionUsageStack 3000 ( 12) : 00.00% 00.03% : RegionUsageTeb 381000 ( 3588) : 00.17% 10.35% : RegionUsageHeap 0 ( 0) : 00.00% 00.00% : RegionUsagePageHeap 1000 ( 4) : 00.00% 00.01% : RegionUsagePeb 1000 ( 4) : 00.00% 00.01% : RegionUsageProcessParametrs 2000 ( 8) : 00.00% 00.02% : RegionUsageEnvironmentBlock Tot: 7fff0000 (2097088 KB) Busy: 021db000 (34668 KB) -------------------- Type SUMMARY -------------------------- TotSize ( KB) Pct(Tots) Usage 7de15000 ( 2062420) : 98.35% : <free> 1452000 ( 20808) : 00.99% : MEM_IMAGE 69f000 ( 6780) : 00.32% : MEM_MAPPED 6ea000 ( 7080) : 00.34% : MEM_PRIVATE -------------------- State SUMMARY -------------------------- TotSize ( KB) Pct(Tots) Usage 1a58000 ( 26976) : 01.29% : MEM_COMMIT 7de15000 ( 2062420) : 98.35% : MEM_FREE 783000 ( 7692) : 00.37% : MEM_RESERVE Largest free region: Base 01432000 - Size 707ee000 (1843128 KB)
Чаще всего можно наблюдать фрагментацию виртуальной памяти, вызванную временными большими объектами, которые требуют от сборщика мусора часто получать новые сегменты управляемой кучи из ОС и освобождать обратно в ОС пустые.
Чтобы убедиться, что LOH вызывает фрагментацию виртуальной памяти, можно установить точку останова в VirtualAlloc и VirtualFree, чтобы узнать, кто их вызвал. Например, чтобы узнать, кто пытался выделить блоки виртуальной памяти размером более 8 МБ из ОС, можно задать точку останова следующим образом:
bp kernel32!virtualalloc "j (dwo(@esp+8)>800000) 'kb';'g'"
Эта команда входит в режим отладки и показывает стек вызовов только если VirtualAlloc вызывается с размером выделения больше 8 МБ (0x800000).
CLR 2.0 добавила функцию под названием удержание виртуальной машины, которая может быть полезна для сценариев, когда сегменты (включая кучи больших и небольших объектов) часто приобретаются и освобождаются. Чтобы указать задержание виртуальных машин, необходимо указать флаг запуска, называемый STARTUP_HOARD_GC_VM
через хостинг API. Вместо освобождения пустых сегментов обратно в ОС среда CLR освобождает память в этих сегментах и помещает их в резервный список. (Обратите внимание, что среда CLR не делает это для сегментов, которые слишком большие.) Среда CLR позже использует эти сегменты для удовлетворения новых запросов сегмента. В следующий раз, когда вашему приложению понадобится новый сегмент, среда CLR использует один из этого резервного списка, если она может найти достаточно большой.
Удержание сегментов виртуальной памяти также полезно для приложений, которые хотят удерживать сегменты, которые они уже получили, например, для серверных приложений, являющихся доминирующими на системе, с целью избежать ошибок из-за недостатка памяти.
Настоятельно рекомендуется тщательно протестировать приложение при использовании этой функции, чтобы обеспечить достаточно стабильное использование памяти в приложении.