Как стать автором
Обновить

Комментарии 22

Спасибо за статью, интересно почитать будет про сборщики в коммерческих JVM еще.
Тема, действительно, интересная. Как позволит время, поразбираемся с проприетарными/коммерческими jvm.
Так всё-таки расскажите почему именно два региона survival и зачем перекладывать объекты между ними?
Представьте себя на месте сборщика. У вас есть регион памяти, который нужно очистить. После удаления мусора регион оказывается сильно дефрагментированным и если вы хотите это исправить, то у вас есть два варианта: либо уплотнять объекты в рамках этого же региона, либо скопировать их в другой, пока еще пустой регион, располагая один-к-одному, а старый регион объявить пустым. Но задача осложняется тем, что объекты ссылаются друг на друга и при перемещении любого объекта необходимо производить обновление всех имеющихся на него ссылок. И вот эту задачу намного легче решать при копировании, причем сразу объединяя ее с задачей поиска живых объектов:

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

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

В итоге мы получаем небольшой оверхед по памяти (пустой Survivor), но зато и высокую скорость работы. Реализация варианта с уплотнением оказывается медленнее. Уплотнение используется при сборках мусора в старшем поколении, так как там уже заведомо старые объекты и мы не можем опираться на гипотезу поколений.
А почему обновление ссылок гораздо легче делать при копировании? Ведь при копировании в новый регион придется изменить все ссылки, а при перемещении в рамках одного региона только те, которые перемещаются. Или имеется ввиду что при копировании не нужно будет проверять, а надо ли обновлять ссылку, а сразу производится эта операция?
Уплотнять объекты в рамках очищаемой области памяти можно двумя способами:

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

2. Вы можете просто перебирать все объекты подряд и копировать их «влево», впритык к предыдущему живому объекту. При этом остается та же самая проблема с необходимостью ведения отдельной структуры под хранение соответствий адресов, что и в первом варианте. И проблема с доп. проходом для обновления адресов тоже никуда не девается.

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

В случае же копирования у нас используется всего один проход по объектам для решения всех трех задач: поиск живых объектов, уплотнение, обновление ссылок.
«вот все так говорят»
а что делать, если у нас из старого поколения есть ссылка на объект из молодого?
получается, что даже при малой сборке надо скопировать в чистую часть сурвайвора все объекты, на которые ссылалось старшее поколение.
// мне тут гаденышь один hystrix servlet повадился 1ГБ/с мусорных объектов создавать и они прорываются (при дефолтных настройках) в сташую область и дико тормозят сборку мусора (жрут процессор), но в статистике JMX выдаются совсем невинные цифры
а что делать, если у нас из старого поколения есть ссылка на объект из молодого?
получается, что даже при малой сборке надо скопировать в чистую часть сурвайвора все объекты, на которые ссылалось старшее поколение


Все так, как раз такой пример со ссылкой из старшего поколения в младшее есть в первой части.

Ссылки из старых объектов на короткоживующие объекты — это не очень хорошо и может увеличивать количество полных сборок. Но при этом перекидывание объекта между survivor'ами прежде чем переместить его в старшее поколение, защищает от таких ситуаций. Ссылка из старшего поколения в младшее должна продержаться достаточно долго и пережить несколько малых циклов сборки, чтобы это привело к «затягиванию» старым объектом молодого в Tenured. А раз она прожила так долго, то и объект уже сложно назвать короткоживущим.

Если все же нежелательные объекты просачиваются в старшее поколение и на уровне кода с этим ничего поделать не удается, можно попробовать увеличить значения InitialTenuringThreshold и MaxTenuringThreshold, заставив объекты созревать в survivor'ах дольше, прежде чем отправиться в tenured. Но все это, конечно, следует делать очень аккуратно, так как увеличение порога созревания приведет к тому, что в survivor'ах будет больше объектов, возможна нехватка места и преждевременное выдавливание объектов в tenured (еще одна причина попадания молодых объектов в старшее поколение), так что может потребоваться и донастройка размеров областей.
Хорошо пишете, спасибо. Про G1GC писать планируете?
Обязательно, в следующей статье будут CMS и G1.
Особенно интересно поведение G1GC при использовании больших короткоживущих объектов (например, больших битсетов в Lucene).

Сам пока не тестировал, но Shawn Heisey из Lucene/Solr пишет, что у него всё неплохо, хотя Lucene guys предостерегают насчёт G1. См. wiki.apache.org/solr/ShawnHeisey#GC_Tuning_for_Solr.
Да, в G1 своя специфика работы с т. н. громадными объектами, о ней в статье обязательно расскажу. И если их много и они короткоживущие, то можно получить неприятности.
Общее замечание про подкручивание опций JVM. По моему опыту, если вы испытываете проблемы со сборкой мусора, это свидетельствует о реальных багах в исполняемом коде (утечки памяти, переполнение очереди финализации из-за большого количества finalize() и так далее). Перед тем как ругать джаву за тормознутость и крутить опции JVM, снимите дамп памяти и поанализируйте его хорошенько.
Согласен, но только отчасти. Я бы перефразировал на "… это часто свидетельствует..." Конечно, в расточительной и протекающей программе нужно прежде всего сосредоточиться на поиске проблем в коде. Но все-таки бывают ситуации, когда можно испытывать проблемы со сборкой мусора и в добротной, абсолютно герметичной программе, не использующей finalizer'ы. А если речь идет о транзакционном сервисе с большой кучей и жестким SLA, то без подкручивания опций JVM редко обходится. Вот мне хабр как раз подсовывает Историю одного garbage collection'а в качестве похожей публикации с примером такой программы. Или вот пример Solr из комментариев выше. Да и не всегда есть возможность код поправить, мы ведь и сторонними инструментами пользуемся.
У меня вопрос по Soft References. Они так же попадают в старшие поколения после виживания в молодом поколения? Weak никогда не виживают, значит они всегда в молодом поколении?
Здесь на всякий случай хотелось бы разобраться с терминологией. Я думаю, вопрос касается не soft/weak references самих по себе, а softly/weakly reachable объектов. При этом важно понимать, что само по себе наличие soft/weak ссылки на объект не делает автоматически этот объект softly/weakly reachable. Таковым его делает только дополнительное отсутствие более сильных ссылок.

И вот к этому моменту, когда более сильных ссылок не осталось и объект стал softly/weakly reachable, он уже может находиться в любом поколении. Так что ответ на второй вопрос формально «нет, они не всегда в молодом поколении». Но если рассматривать случай, когда на объект изначально не существовало никаких ссылок, кроме weak, он не перейдет в более старшее поколение, так как ссылка на него будет обнулена при первой же сборке мусора.

Что касается softly reachable объектов, по ним у GC есть только одно формальное требование — обнулить их перед выбрасыванием OOM в рамках последней попытки освободить память. Но при этом нет никакого запрета на то, чтобы они вели себя полностью аналогично weak reachable. На практике, конечно, GC старается найти золотую середину, не выбрасывая их сразу при обнаружении, но и не оттягивая очистку до последнего. Поэтому такие объекты спокойно переходят в старшее поколение если недостатка в памяти нет. Посмотрите описание опции SoftRefLRUPolicyMSPerMB, она регулирует поведение GC в части определения, когда уже можно удалять softly reachable объекты.
когда более сильных ссылок не осталось и объект стал softly/weakly reachable, он уже может находиться в любом поколении.

это проясняет многое для меня

Поэтому такие объекты [softly reachable] спокойно переходят в старшее поколение если недостатка в памяти нет

нигде не могу найти подтверждение этому, только немного здесь
нигде не могу найти подтверждение этому, только немного здесь

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

Но описание опции SoftRefLRUPolicyMSPerMB дает чуть больше информации об обработке таких объектов в HotSpot. В частности, что время жизни softly referenced объектов зависит от частоты их использования и от объема свободной памяти. Но никак не от того, в каком поколении находятся соответствующие объекты. Без возможности перемещения в старшее поколение они бы не отличались от weak reference объектов и кэширование на их основе было бы менее эффективным.
Без возможности перемещения в старшее поколение они бы не отличались от weak reference объектов и кэширование на их основе было бы менее эффективным.


Разве поведение GC c softly reachable объектами отличается в зависимости от того где они находятся? Я наверное неправильно понимаю Ваше предложение
Нет, не отличается. Вы написали, что не можете найти подтверждение тому, что softly reachable объекты переходят в старшее поколение. И в качестве такого подтверждения, помимо указаний на технические детали реализации, я хотел донести мысль, что если бы им было запрещено туда переходить, то их основная цель (создание кэшей, гибко подстраивающихся под имеющиеся объемы памяти) не достигалась бы и они просто дублировали бы поведение weakly reachable объектов.
Просто ничто не запрещает коллектору оставить softly reachable объект в текущем поколении после решения не выкидывать его. Интересно делает ли так HotSpot (например, оставлять softly в Edem, с целью чаще смотреть нужно ли выкидывать такие объекты)
HotSpot так не делает.

На самом деле, идея ограничивать возможность продвижения таких объектов в старшее поколение не несет практически никаких преимуществ. Редко используемый softly reachable объект может жить в памяти долго только при условии, что у приложения достаточно много памяти, чтобы позволить себе поддерживать его жизнеспособность. И это хорошо, ведь это кэш, объект может ведь и понадобиться рано или поздно. А как только памяти станет не хватать, такие объекты постепенно начнут выдавливаться из памяти в порядке возрастания частоты использования.

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

Публикации