Теория и практика Java: Сборка мусора в HotSpot JVM

Основанная на поколениях и одновременная сборка мусора

В последнем выпуске Теории и практики Java автор Брайан Гетц провел обзор основных алгоритмов сборки мусора. В этом месяце он делает шаг вперед и анализирует, каким образом 1.4.1 JVM выполняет сборку мусора, а также рассказывает о некоторых новых возможностях сборки мусора для многопроцессорных систем.

Брайан Гетц, главный консультант, Quiotix

Брайан Гетц (Brian Goetz) последние 15 лет работает профессиональным разработчиком ПО. Сейчас он является главным консультантом в фирме Quiotix, занимающейся разработкой ПО и консалтингом и находящейся в Лос-Альтос, Калифорния. Он также является членом нескольких экспертных групп JCP (JCP Expert Groups). См. его опубликованные и готовые к публикации статьи в популярных отраслевых изданиях. Вы можете связаться с ним по адресу brian@quiotix.com.



14.03.2007

В последнем выпуске мы рассмотрели классические приемы сборки мусора: подсчет ссылок, копирование, маркирование-чистку и маркирование-сжатие. У каждого из этих приемов есть как преимущества, так и недостатки в определенных ситуациях. Например, копирование хорошо работает, когда большой процент объектов является мусором, но плохо работает с большим количеством долгоживущих объектов (периодически выполняя их копирование). Наоборот, маркировка-сжатие вполне хорошо работает с долгоживущими объектами (копируя их только один раз), но не так хорошо с большим количеством объектов, живущих недолго. Данный метод, использованный в версии 1.2 и более поздних версиях JVM, называется основанной на поколениях сборкой мусора и соединяет в себе два метода для того, чтобы взять все самое лучшее от обоих и, дополнительно обеспечивает очень низкие затраты на размещение объектов.

Старые объекты, молодые объекты

В динамической памяти приложения некоторые объекты становятся мусором вскоре после создания, некоторые живут в течение долгого времени и только затем становятся мусором, другие могут остаться в живых в течение всего времени работы программы. Эмпирические исследования показали, что в большинстве объектно-ориентированных языков, включая Java, огромное количество объектов, приблизительно 98 % (в зависимости от Ваших показателей молодости объекта), умирают молодыми. Возраст объектов можно измерять в секундах настенных часов, в общем количестве байтов, выделенных подсистемой управления памятью, с тех пор, как объект был размещен, или в числе сборок мусора с момента размещения объекта. Но как бы Вы не измеряли, большинство исследований показывает, что объекты умирают молодыми. Тот факт, что объекты умирают молодыми, важен для выбора сборщика. В частности, копирующие сборщики мусора работают довольно хорошо, когда большинство объектов умирает молодыми, так как копирующие сборщики совершенно не посещают мертвые объекты; они просто копируют живые объекты в другие участки динамической памяти, и затем целиком возвращают оставшееся место в одной области.

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


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

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

Вспомогательные сборки мусора

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

Ссылки между разными поколениями

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

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

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

Отслеживание ссылок между разными поколениями

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

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

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

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

На рисунке 1, стрелки отражают ссылки между объектами в динамической памяти. Красные ссылки отображают ссылки старых объектов на молодые, которые должны быть добавлены к корневому набору для вспомогательной сборки мусора. Синие стрелки отображают ссылки на старые объекты либо из корневого набора, либо из молодого поколения, которые не нужно трассировать при обработке только молодого поколения.

Рисунок 1. Ссылки между поколениями
Ссылки между поколениями

Маркировка карт

В пакетах Sun JDK используется оптимизированный вариант алгоритма, называемый маркировкой карт, для определения изменения указателей, содержащихся в полях объектов старого поколения. В данном подходе динамическая память разделена на набор карт, каждая, из которых, обычно меньше, чем страница памяти. Виртуальная машина поддерживает схему карт, на которой один бит (или байт в некоторых версиях) соответствует одной карте в динамической памяти. При каждом изменении поля указателя объекта в динамической памяти, в схеме карт устанавливается соответствующий бит. Во время сборки мусора, маркирующие биты, связанные с картами в старом поколении проверяются, и грязные карты сканируются для нахождения объектов, содержащих ссылки на более молодое поколение. Затем эти биты стираются. Разметка на карты требует определенных затрат: дополнительного пространства для схемы карт, дополнительной работы, которая проделывается над каждым хранилищем указателя, и дополнительной работы во время сборки мусора. Алгоритмы маркировки карт могут добавить только три-четыре машинные команды на одно неинициализирующее хранилище указателя в динамической памяти. Это влечет за собой сканирование любых объектов в грязных картах во время вспомогательной сборки мусора.


Сборщик мусора в пакете JDK 1.4.1 по умолчанию

По умолчанию пакет 1.4.1 JDK делит динамическую память на два сектора: молодое поколение и старое поколение. (На самом деле, есть еще третий сектор - постоянная область, которая используется для хранения загруженных объектов методов и классов). Молодое поколение делится на область порождения, часто называемую Эдемом, и два полупространства выживших объектов, в которых используется копирующий сборщик мусора.

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

Другие сборщики мусора

Кроме копирующего и маркирующе-сжимающего сборщиков мусора, которые используются по умолчанию, в пакет 1.4.1 JDK также присутствуют четыре других алгоритма сборки мусора, каждый из которых используется для разных целей. JDK 1.4.1 содержит инкрементный сборщик (который применяется еще со времен пакета JDK 1.2) и три новых сборщика для более эффективной работы на многопроцессорных системах: параллельный копирующий сборщик мусора, параллельный утилизирующий сборщик мусора и синхронизированный маркирующе-чистящий сборщик. Эти новые сборщики призваны решить проблему сборки мусора, которая является основным припятствием для решения проблемы масштабируемости на многопроцессорных системах. В таблице 2 приведены некоторые рекомендации по выбору между различными сборщиками мусора.

Таблица 2. Варианты сборки мусора 1.4.1 (публикуется с разрешения Folgmann IT-Consulting)
Варианты сборки мусора

Инкрементная сборка мусора

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

Алгоритм, используемый в JDK для инкрементной сборки мусора, так называемый алгоритм Train (поезд), создает в динамической памяти новую область между старым и новым поколениями. Эти области делятся на "составы", каждый из которых делится на последовательности "вагонов". Каждый вагон может обрабатываться сборщиком мусора отдельно. Фактически, каждый вагон представляет отдельное поколение. Это значит, что должны быть отслежены не только ссылки старых объектов на молодые, но и ссылки старых составов на молодые составы и старых вагонов на молодые вагоны. Это приводит к появлению достаточного количества дополнительной работы и для мутатора, и для сборщика мусора, но сокращает паузы на сборку мусора.

Параллельные и синхронизированные сборщики мусора

Новые сборщики в пакете JDK 1.4.1 разработаны для решения проблем со сборкой мусора на многопроцессорных системах. Так как большинство алгоритмов сборки мусора останавливают среду реализации на некоторый период времени, то однопотоковый сборщик мусора может быстро стать помехой для масштабируемости, так как все кроме одного процессора не работают, пока сборщик мусора приостановил потоки пользовательской программы. Два новых сборщика мусора созданы, чтобы уменьшить время, затрачиваемое на паузы: параллельный копирующий и синхронизированный маркирующе-чистящий сборщики. А параллельный утилизирующий сборщик мусора, создан для обеспечения большей производительности на больших объемах динамической памяти.

Параллельный копирующий сборщик мусора, вызываемый параметром -XX:+UseParNewGC JVM, является копирующим сборщиком мусора молодого поколения, который разделяет сборку мусора между таким количеством потоков, какое имеется на процессоре. Синхронизированный маркирующе-чистящий сборщик, вызываемый -XX:+UseConcMarkSweepGC , является маркирующе-чистящим сборщиком старого поколения, который на краткий промежуток времени останавливает среду выполнения для начальной фазы маркировки (и еще раз позже для краткой повторной маркировки) и затем позволяет пользовательской программе возобновить работу, пока потоки сборщика мусора выполняются синхронно с пользовательской программой. Параллельный копирующий и синхронизированный маркирующе-чистящий сборщики по своей сути являются синхронизированными версиями используемых по умолчанию копирующего и маркирующе-сжимающего сборщиков мусора. Параллельный утилизирующий сборщик, вызываемый -XX:+UseParallelGC, является сборщиком молодого поколения, оптимизированным для очень больших (гигабайт и более) объемов динамической памяти на многопроцессорных системах.

Выбор алгоритма

Имея в наличии 6 алгоритмов у Вас, вероятно, возникает вопрос, как выбрать. В таблице 2, которая разделяет сборщики на однопотоковые и синхронизированные, и на сборщики с короткими паузами и высокой производительностью, приведены некоторые рекомендации. Учитывая то, что Вы знаете о Вашем приложении и среде выполнения, данной информации может быть достаточно для выбора соответствующего алгоритма. Для многих приложений сборщики мусора, заданные по умолчанию, работаю вполне приемлемо, поэтому если у Вас нет проблемы с производительностью, то ни к чему усложнять себе жизнь. Однако если Ваше приложение работает на многопроцессорной системе или использует очень большую динамическую память, то, изменив сборщик мусора, Вы можете добиться повышения производительности.


Настройка сборщика мусора

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

Настройку сборки мусора нужно, прежде всего, начинать с анализа вербальных выходных данных сборщика. Это даст Вам информацию о частотности, временных параметрах и длительности операций по сборки мусора. Простейший способ настройки сборки мусора заключается в простом расширении максимального размера динамической памяти (-Xmx). Работа копирующего сборщика мусора становится более эффективной с ростом динамической памяти, поэтому расширяя динамическую память, Вы сокращаете расходы на сборку мусора в расчете на один объект. Помимо расширения максимального размера динамической памяти Вы можете также использовать опцию -XX:NewRatio для увеличения размеров области, выделяемой для молодого поколения. Вы также можете напрямую задать размер молодого поколения с помощью опции -Xmn. В разделе Ресурсы представлены статьи, предлагающие более детальные советы по настройке сборки мусора.


Заключение

С развитием виртуальных машин Java работающие по умолчанию сборщики мусора становятся все лучше и лучше. Основанный на поколениях и используемый пакетом JDK версии 1.2 и более поздних версий, предлагает лучшее размещение объектов и эффективность сборки мусора, чем маркирующе-чистяще-сжимающий сборщик мусора, используемый в пакетах JDK более ранних версий. Пакет 1.4.1 JDK еще более увеличивает эффективность сборки мусора добавлением новых опций для многопоточной сборки в многопроцессорных системах и динамической памяти очень больших объемов.

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

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=201968
ArticleTitle=Теория и практика Java: Сборка мусора в HotSpot JVM
publish-date=03142007