...Перед тем, как мы разберем варианты по ручному управлению GC, давайте пока ознакомимся с общей структурой алгоритма подчистки памяти сборщиком мусора.
Мы уже знаем, что сборка мусора представляет собой непрерывный процесс слежения за состоянием памяти. Однако, активную фазу принимает в основном в момент острого недостатка памяти для продолжения работы приложения. Кроме того, следует помнить, что сборщик мусора всегда запускается в отдельном потоке и в момент активной работы его, все остальные потоки приложения приостанавливаются.
Итак, предположим, в данный момент CLR принялась искать неиспользуемые объекты для освобождения необходимого объема памяти. Но CLR не проходит по всем находящимся в памяти объектам и не изучает их. Вместо этого CLR выполняет проход по определенной группе объектов.
Рисунок 1. Поколения объектов
В .NET эти группы называются поколениями. Всего существует 3 поколения:
- Поколение 0. Новый созданный объект, который еще никогда не помечался как кандидат на удаление;
- Поколение 1. Объект, который уже однажды пережил сборку мусора. Сюда обычно попадают объекты, которые были помечены для удаления, но все-таки не удалены из-за того, что места в памяти (куче) было предостаточно;
- Поколение 2. Объекты, которые пережили более одной очистки памяти сборщиком мусора.
Очевидно, что чем выше поколение объекта, то тем дольше он находится в памяти. Более того, вероятность дальнейшего его существования в куче больше, чем у объекта более низкого поколения.
Но почему сборщик мусора сразу не удаляет всех кандидатов на удаление, а исследует только определенную группу? Вопрос скрыт в производительности. Вспомним, что процесс сборки мусора приостанавливает остальные программные потоки. Если потоки для Вас еще не знакомы, то можете себе этот процесс понять таким образом: как только сборщик мусора начал удаление объектов, ваша программа зависает до тех пор, пока сборщик не прекратит процесс освобождения памяти. Ведь, для исследования объекта и последующего удаления последнего требуется довольно длительное время. Поэтому, для повышения производительности сборка мусора работает на достаточном уровне, то есть как только получена требуемая порция свободной памяти, удаление объектов прекращается.
Вначале CLR исследует поколение 0. Если в процессе удаления неиспользуемых объектов из поколения 0 удалось высвободить требуемый объем памяти, все выжившие объекты переходят в поколение 1. Если же после подчистки поколения 0 не удалось высвободить требуемый объем памяти, CLR принимается за поколение 1. Соответственно, если после удаления неиспользуемых объектов в поколении 1 удалось освободить необходимый объем памяти, все выжившие объекты переходят в поколение 2. Если же достаточное количество памяти не получено, сборщик мусора принимается за последнее поколение. Объекты, пережившие зачистку в поколении 2 не переходят выше, а остаются в данном поколении (поколение 2 является самым высоким уровнем).
Но почему алгоритм подчистки памяти работает именно с поколениями, а не просто с неиспользуемыми объектами? Зачем как то классифицировать объекты по поколениям? Причина такого подхода состоит в том, что в программах обычно присутствуют сущности со своими уровнями видимости для переменных. Например, область видимости переменных класса/объекта (поля) является больше локальных переменных некоторых классовых объектов, или их параметров, или тем более циклов. Соответственно и период жизни у них сильно отличается. Поэтому, при назначении номеров поколений объектам в управляемой куче новые объекты (такие как локальные переменные) быстро удаляются, а более старые объекты (статические переменные, поля, объекты самого приложения и т.д.) живут дольше, соответственно достигают более высоких поколений, что продлевает их срок жизни. Даже не продлевает срок жизни, а указывает CLR рассматривать их в последнюю очередь.