Реализация сборщиков мусора в Java

Одним из преимуществ платформы Java SE является то, что она защищает разработчика от сложности выделения памяти и сборки мусора.

Однако, когда сборка мусора является основным узким местом, полезно понять некоторые аспекты реализации. Сборщики мусора делают предположения о том, как приложения используют объекты, и это отражается в настраиваемых параметрах, которые можно настраивать для повышения производительности без ущерба для мощности абстракции.

Сборка мусора по поколениям

Объект считается мусором, и его память может быть повторно использована виртуальной машиной, если к нему больше нельзя получить доступ по какой-либо ссылке из любого другого живого объекта в работающей программе.

Теоретический, самый простой алгоритм сборки мусора перебирает каждый достижимый объект при каждом запуске. Любые оставшиеся объекты считаются мусором. Время, которое занимает этот подход, пропорционально количеству живых объектов, что непомерно для больших приложений, поддерживающих большое количество живых данных.

Java HotSpot VM включает в себя ряд различных алгоритмов сборки мусора, которые все используют технику, называемую сборкой поколений (generational collection). Хотя наивная сборка мусора каждый раз проверяет каждый живой объект в куче, сборка поколений использует несколько эмпирически наблюдаемых свойств большинства приложений, чтобы минимизировать работу, необходимую для восстановления неиспользуемых (мусорных) объектов. Наиболее важным из этих наблюдаемых свойств является гипотеза слабого поколения, которая гласит, что большинство объектов выживают только в течение короткого периода времени.

Синяя область на рисунке является типичным распределением времени жизни объектов. Ось X показывает время жизни объекта, измеренное в выделенных байтах. Число байтов на оси Y - это общее количество байтов в объектах с соответствующим временем жизни. Острый пик слева представляет объекты, которые могут быть восстановлены (другими словами, "умерли") вскоре после распределения. Например, объекты итератора часто работают только в течение одного цикла.

Некоторые объекты живут дольше, и поэтому распределение простирается вправо. Например, обычно есть некоторые объекты, выделенные при инициализации, которые живут до выхода из VM. Между этими двумя крайностями находятся объекты, которые живут в течение некоторого промежуточного вычисления, рассматриваемого здесь как холм справа от исходного пика. Некоторые приложения имеют очень по-разному выглядящие распределения, но удивительно большое количество обладает этой общей формой. Эффективный сбор стал возможен благодаря тому, что большинство предметов "умирают молодыми".

Поколения

Чтобы оптимизировать этот сценарий, память управляется поколениями (пулы памяти, содержащие объекты разного возраста). Сборка мусора происходит в каждом поколении, когда поколение заполняется.

Подавляющее большинство объектов размещено в пуле, предназначенном для молодых объектов (молодого поколения), и большинство объектов погибает там. Когда молодое поколение заполняется, это вызывает малую сборку (minor collection), в которой собирается только молодое поколение; мусор в других поколениях не утилизируется. Стоимость таких сборок в первом порядке пропорциональна количеству собираемых живых объектов; молодое поколение, полное мертвых объектов, собирается очень быстро. Как правило, некоторая часть выживших объектов молодого поколения перемещается к старому поколению во время каждой малой сборки. В конце концов, старое поколение заполняется и должно быть собрано, в результате чего выполняется большая сборка (major collection), в которой собирается вся куча. Большие сборки обычно длятся намного дольше, чем малые, потому что задействовано значительно большее количество объектов. На рисунке показано расположение поколений по умолчанию в последовательном сборщике мусора (serial garbage collector):

При запуске виртуальная машина Java HotSpot резервирует всю кучу Java в адресном пространстве, но не выделяет для нее физической памяти, если в этом нет необходимости. Все адресное пространство, охватывающее кучу Java, логически разделено на молодое и старое поколения. Полное адресное пространство, зарезервированное для памяти объекта, можно разделить на молодое и старое поколения.

Молодое поколение состоит из Eden и двух пространств выживших (Survivor). Большинство объектов изначально расположены в Eden. Одно пространство для выживших в любое время пусто и служит местом назначения живых объектов в eden, а другое пространство для выживших во время сбора мусора; после сборки мусора eden и исходное пространство оставшихся в живых остаются пустыми. В следующей сборке мусора цель двух оставшихся в живых мест заменяется. Одно недавно заполненное пространство является источником живых объектов, которые копируются в другое пространство выживших. Таким образом, объекты копируются между оставшимися в живых пространствами до тех пор, пока они не будут скопированы определенное количество раз или пока там не останется достаточно места. Эти объекты копируются в старый регион. Этот процесс также называется старением.

Вопросы производительности

Основными показателями сбора мусора являются пропускная способность (throughput) и задержка (latency).

Пропускная способность - это процент от общего времени, не затраченного на сборку мусора, рассматриваемого в течение длительных периодов времени. Пропускная способность включает время, затрачиваемое на распределение (но настройка скорости распределения обычно не требуется).

Задержка - это отзывчивость приложения. Паузы сбора мусора влияют на отзывчивость приложений.

У пользователей разные требования к сборке мусора. Например, некоторые считают правильную метрику для веб-сервера пропускной способностью, потому что паузы во время сборки мусора могут быть терпимы или просто скрыты сетевыми задержками. Однако в интерактивной графической программе даже короткие паузы могут отрицательно повлиять на работу пользователя.

Некоторые пользователи чувствительны к другим соображениям. Footprint (площадь) - это рабочий набор процесса, измеряемый в страницах и строках кэша. В системах с ограниченной физической памятью или многими процессами занимаемая площадь может определять масштабируемость. Оперативность (Promptness) - это время между тем, когда объект становится мертвым, и когда память становится доступной, что является важным фактором для распределенных систем, включая удаленный вызов метода (RMI).

В общем, выбор размера для конкретного поколения является компромиссом между этими соображениями. Например, очень большое молодое поколение может максимизировать пропускную способность, но делает это за счет затрат времени, скорости и пауз. Паузы молодого поколения можно минимизировать, если использовать небольшое молодое поколение за счет пропускной способности. Размер одного поколения не влияет на частоту сбора и время паузы для другого поколения.

Не существует единственного правильного способа выбрать размер поколения. Лучший выбор определяется тем, как приложение использует память, а также требованиями пользователя. Таким образом, выбор сборщиком мусора на виртуальной машине не всегда оптимален и может быть переопределен параметрами командной строки.

Измерение пропускной способности и занимаемой площади

Пропускная способность и занимаемая площадь лучше всего измеряются с помощью показателей, специфичных для приложения.

Например, пропускная способность веб-сервера может быть проверена с использованием генератора нагрузки клиента. Однако паузы из-за сборки мусора легко оцениваются путем проверки диагностического вывода самой виртуальной машины. Параметр командной строки -verbose:gc печатает информацию о куче и сборке мусора в каждой коллекции. Вот пример:

[15,651s][info ][gc] GC(36) Pause Young (G1 Evacuation Pause) 239M->57M(307M) (15,646s, 15,651s) 5,048ms
[16,162s][info ][gc] GC(37) Pause Young (G1 Evacuation Pause) 238M->57M(307M) (16,146s, 16,162s) 16,565ms
[16,367s][info ][gc] GC(38) Pause Full (System.gc()) 69M->31M(104M) (16,202s, 16,367s) 164,581ms

Выходные данные показывают две молодые коллекции, за которыми следует полная коллекция, которая была инициирована приложением с помощью вызова System.gc(). Строки начинаются с отметки времени, указывающей время с момента запуска приложения. Далее идет информация об уровне журнала (info) и теге (gc) для этой строки. Затем следует идентификационный номер GC. В этом случае есть три GC с номерами 36, 37 и 38. Затем регистрируется тип GC и причина для определения GC. После этого регистрируется некоторая информация о потреблении памяти. В этом журнале используется формат "использовалось до GC" -> "используется после GC" ("размер кучи").

В первой строке примера это 239M->57M(307M), что означает, что 239 МБ были использованы до сборки мусора и сборщик мусора очистил большую часть этой памяти, но 57 МБ выжили. Размер кучи составляет 307 МБ. Обратите внимание, что в этом примере полная сборка мусора сокращает кучу с 307 МБ до 104 МБ. После информации об использовании памяти, время начала и окончания для GC регистрируется, а также продолжительность (конец - начало).

Команда -verbose:gc является псевдонимом для -Xlog:gc. -Xlog - это общий параметр конфигурации ведения журнала для входа в HotSpot JVM. Это система на основе тегов, где gc является одним из тегов. Чтобы получить больше информации о том, что делает GC, вы можете настроить ведение журнала для печати любого сообщения, имеющего тег gc и любой другой тег. Параметр командной строки для этого -Xlog:gc*.

Вот пример одной молодой коллекции G1, зарегистрированной с -Xlog:gc*:

[10.178s][info][gc,start ] GC(36) Pause Young (G1 Evacuation Pause) 
[10.178s][info][gc,task ] GC(36) Using 28 workers of 28 for evacuation 
[10.191s][info][gc,phases ] GC(36) Pre Evacuate Collection Set: 0.0ms
[10.191s][info][gc,phases ] GC(36) Evacuate Collection Set: 6.9ms 
[10.191s][info][gc,phases ] GC(36) Post Evacuate Collection Set: 5.9ms 
[10.191s][info][gc,phases ] GC(36) Other: 0.2ms 
[10.191s][info][gc,heap ] GC(36) Eden regions: 286->0(276) 
[10.191s][info][gc,heap ] GC(36) Survivor regions: 15->26(38)
[10.191s][info][gc,heap ] GC(36) Old regions: 88->88 
[10.191s][info][gc,heap ] GC(36) Humongous regions: 3->1 
[10.191s][info][gc,metaspace ] GC(36) Metaspace: 8152K->8152K(1056768K)
[10.191s][info][gc ] GC(36) Pause Young (G1 Evacuation Pause) 391M->114M(508M) 13.075ms 
[10.191s][info][gc,cpu ] GC(36) User=0.20s Sys=0.00s Real=0.01s


Читайте также:


Комментарии

Популярные сообщения из этого блога

Методы класса Object в Java

Как получить текущий timestamp в Java

Основные опции JVM для повышения производительности и отладки