Дюк, вынеси мусор! — Часть 3

  • Tutorial
Часть 3 - CMS GC и G1 GC

Сегодня мы продолжаем цикл статей о сборщиках мусора, поставляемых с виртуальной машиной Oracle Java HotSpot VM. Мы уже изучили немного теории и рассмотрели, каким образом с кучей расправляются два базовых сборщика — Serial GC и Parallel GC. А в этой статье речь пойдет о сборщиках CMS GC и G1 GC, первостепенной задачей которых является минимизация пауз при наведении порядка в памяти приложений, оперирующих средними и большими объемами данных, то есть по большей части в памяти серверных приложений.

Два этих сборщика объединяют общим названием «mostly concurrent collectors», то есть «по большей части конкурентные сборщики». Это связано с тем, что часть своей работы они выполняют параллельно с основными потоками приложения, то есть в какие-то моменты конкурируют с ними за ресурсы процессора. Конечно, это не проходит бесследно, и в итоге они разменивают улучшение в части пауз на ухудшение в части пропускной способности. Хотя делают это по-разному. Давайте посмотрим, как.

CMS GC


Сборщик CMS (расшифровывается как Concurrent Mark Sweep) появился в HotSpot VM в одно время с Parallel GC в качестве его альтернативы для использования в приложениях, имеющих доступ к нескольким ядрам процессора и чувствительных к паузам STW. В то время существовала еще одна альтернатива — Incremental GC, но он не прошел естественный отбор за неимением явных преимуществ. А CMS выжил. И хотя пик его популярности, видимо, уже прошел, на его внутреннее устройство интересно будет взглянуть, так как некоторые заложенные в него идеи перекочевали в более современный G1 GC.

Использование CMS GC включается опцией -XX:+UseConcMarkSweepGC.

Принципы работы


Мы уже встречали слова Mark и Sweep при рассмотрении последовательного и параллельного сборщиков (если вы не встречали, то сейчас как раз самое время это сделать). Они обозначали два шага в процессе сборки мусора в старшем поколении: пометку выживших объектов и удаление мертвых объектов. Сборщик CMS получил свое название благодаря тому, что выполняет указанные шаги параллельно с работой основной программы.

При этом CMS GC использует ту же самую организацию памяти, что и уже рассмотренные Serial / Parallel GC: регионы Eden + Survivor 0 + Survivor 1 + Tenured и такие же принципы малой сборки мусора. Отличия начинаются только тогда, когда дело доходит до полной сборки. В случае CMS ее называют старшей (major) сборкой, а не полной, так как она не затрагивает объекты младшего поколения. В результате, малая и старшая сборки здесь всегда разделены. Одним из побочных эффектов такого разделения является то, что все объекты младшего поколения (даже потенциально мертвые) могут играть роль корней при определении статуса объектов в старшем поколении.

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

Давайте рассмотрим, что из себя представляет старшая сборка мусора при использовании CMS GC.

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

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

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

Потоки при работе сборщика CMS

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

Отдельно следует рассмотреть ситуацию, когда сборщик не успевает очистить Tenured до того момента, как память полностью заканчивается. В этом случае работа приложения останавливается, и вся сборка производится в последовательном режиме. Такая ситуация называется сбоем конкурентного режима (concurrent mode failure). Сборщик сообщает нам об этих сбоях при включенных опциях -verbose:gc или -Xloggc:filename.

У CMS есть один интересный режим работы, называемый Incremental Mode, или i-cms, который заставляет его временно останавливаться при выполнении работ параллельно с основным приложением, чтобы на короткие периоды высвобождать ресурсы процессора (что-то вроде АБС у автомобиля). Это может быть полезным на машинах с малым количеством ядер. Но данный режим уже помечен как не рекомендуемый к применению и может быть отключен в будущих релизах, поэтому подробно его разбирать не будем.

Ситуации STW


Из всего сказанного выше следует, что при обычной сборке мусора у CMS GC существуют следующие ситуации, приводящие к STW:
  • Малая сборка мусора. Эта пауза ничем не отличается от аналогичной паузы в Parallel GC.
  • Начальная фаза поиска живых объектов при старшей сборке (так называемая initial mark pause). Эта пауза обычно очень короткая.
  • Фаза дополнения набора живых объектов при старшей сборке (известная также как remark pause). Она обычно длиннее начальной фазы поиска.

В случае же возникновения сбоя конкурентного режима пауза может затянуться на достаточно длительное время.

Настройка


Так как подходы к организации памяти у CMS аналогичны используемым в Serial / Parallel GC, для него применимы те же опции определения размеров регионов кучи, а также опции автоматической подстройки под требуемые параметры производительности.

Обычно CMS, основываясь на собираемой статистике о поведении приложения, сам определяет, когда ему выполнять старшую сборку, но у него также есть порог наполненности региона Tenured, при достижении которого должна обязательно быть инициирована старшая сборка. Этот порог можно задать с помощью опции -XX:CMSInitiatingOccupancyFraction=?, значение указывается в процентах. Значение -1 (иногда устанавливается по умолчанию) указывает на отключение сборки по такому условию.

Достоинства и недостатки


Достоинством данного сборщика по сравнению с рассмотренными ранее Serial / Parallel GC является его ориентированность на минимизацию времен простоя, что является критическим фактором для многих приложений. Но для выполнения этой задачи приходится жертвовать ресурсами процессора и зачастую общей пропускной способностью.

Вспомним еще, что данный сборщик не уплотняет объекты в старшем поколении, что приводит к фрагментации Tenured. Этот факт в совокупности с наличием плавающего мусора приводит к необходимости выделять приложению (конкретно — старшему поколению) больше памяти, чем потребовалось бы для других сборщиков (Oracle советует на 20% больше).

Ну и долгие паузы при потенциально возможных сбоях конкурентного режима могут стать неприятным сюрпризом. Хотя они не частые, и при наличии достаточного объема памяти CMS’у удается их полностью избегать.

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



G1 GC


Вот мы и добрались до последнего и наверняка самого интересного для многих сборщика мусора — G1 (что является сокращением от Garbage First). Интересен он прежде всего тем, что не является явным продолжением линейки Serial / Parallel / CMS, добавляющим параллельность еще в какую-нибудь фазу сборки мусора, а использует уже существенно отличающийся подход к задаче очистки памяти.

G1 — самый молодой в составе сборщиков мусора виртуальной машины HotSpot. Он изначально позиционировался как сборщик для приложений с большими кучами (от 4 ГБ и выше), для которых важно сохранять время отклика небольшим и предсказуемым, пусть даже за счет уменьшения пропускной способности. На этом поле он конкурировал с CMS GC, хотя изначально и не так успешно, как хотелось бы. Но постепенно он исправлялся, улучшался, стабилизировался и, наконец, достиг такого уровня, что Oracle говорит о нем как о долгосрочной замене CMS, а в Open JDK даже серьезно рассматривают его на роль сборщика по умолчанию для серверных конфигураций в 9-й версии.

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

G1 включается опцией Java -XX:+UseG1GC.

Принципы работы


Первое, что бросается в глаза при рассмотрении G1 — это изменение подхода к организации кучи. Здесь память разбивается на множество регионов одинакового размера. Размер этих регионов зависит от общего размера кучи и по умолчанию выбирается так, чтобы их было не больше 2048, обычно получается от 1 до 32 МБ. Исключение составляют только так называемые громадные (humongous) регионы, которые создаются объединением обычных регионов для размещения очень больших объектов.

Разделение регионов на Eden, Survivor и Tenured в данном случае логическое, регионы одного поколения не обязаны идти подряд и даже могут менять свою принадлежность к тому или иному поколению. Пример разделения кучи на регионы может выглядеть следующим образом (количество регионов сильно приуменьшено):

Регионы сборщика G1 GC

Малые сборки выполняются периодически для очистки младшего поколения и переноса объектов в регионы Survivor, либо их повышения до старшего поколения с переносом в Tenured. Над переносом объектов трудятся несколько потоков, и на время этого процесса работа основного приложения останавливается. Это уже знакомый нам подход из рассмотренных ранее сборщиков, но отличие состоит в том, что очистка выполняется не на всем поколении, а только на части регионов, которые сборщик сможет очистить не превышая желаемого времени. При этом он выбирает для очистки те регионы, в которых, по его мнению, скопилось наибольшее количество мусора и очистка которых принесет наибольший результат. Отсюда как раз название Garbage First — мусор в первую очередь.

А с полной сборкой (точнее, здесь она называется смешанной (mixed)) все немного хитроумнее, чем в рассмотренных ранее сборщиках. В G1 существует процесс, называемый циклом пометки (marking cycle), который работает параллельно с основным приложением и составляет список живых объектов. За исключением последнего пункта, этот процесс выглядит уже знакомо для нас:
  1. Initial mark. Пометка корней (с остановкой основного приложения) с использованием информации, полученной из малых сборок.
  2. Concurrent marking. Пометка всех живых объектов в куче в нескольких потоках, параллельно с работой основного приложения.
  3. Remark. Дополнительный поиск не учтенных ранее живых объектов (с остановкой основного приложения).
  4. Cleanup. Очистка вспомогательных структур учета ссылок на объекты и поиск пустых регионов, которые уже можно использовать для размещения новых объектов. Первая часть этого шага выполняется при остановленном основном приложении.

Следует иметь в виду, что для получения списка живых объектов G1 использует алгоритм Snapshot-At-The-Beginning (SATB), то есть в список живых попадают все объекты, которые были таковыми на момент начала работы алгоритма, плюс все объекты, созданные за время его выполнения. Это, в частности, означает, что G1 допускает наличие плавающего мусора, с которым мы познакомились при рассмотрении сборщика CMS.

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

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

Смешанная сборка мусора в приведенном выше примере кучи может пройти вот так:

Смешанная сборка в G1 GC

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

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

Гиганты


В начале рассказа о G1 я упомянул о существовании громадных регионов, в которых хранятся так называемые громадные объекты (humongous objects). С точки зрения JVM любой объект размером больше половины региона считается громадным и обрабатывается специальным образом:
  • Он никогда не перемещается между регионами.
  • Он может удаляться в рамках цикла пометки или полной сборки мусора.
  • В регион, занятый громадным объектом, больше никого не подселяют, даже если в нем остается свободное место.

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

В продолжении данного цикла статей мы посмотрим, как с этим можно бороться.

Ситуации STW


Если резюмировать, то у G1 мы получаем STW в следующих случаях:
  1. Процессы переноса объектов между поколениями. Для минимизации таких пауз G1 использует несколько потоков.
  2. Короткая фаза начальной пометки корней в рамках цикла пометки.
  3. Более длинная пауза в конце фазы remark и в начале фазы cleanup цикла пометки.


Настройка


Так как основной целью сборщика G1 является минимизация пауз в работе основного приложения, то и главной опцией при его настройке можно считать уже встречавшуюся нам -XX:MaxGCPauseMillis=?, задающую приемлемое для нас максимальное время разовой сборки мусора. Даже если вы не собираетесь задавать это свойство, хотя бы проверьте его значение по умолчанию. Хотя в документации Oracle и говориться, что по умолчанию время сборки не ограничено, но по факту это не всегда так.

Опции -XX:ParallelGCThreads=? и -XX:ConcGCThreads=? задают количество потоков, которые будут использоваться для сборки мусора и для выполнения цикла пометок соответственно.

Если вас не устраивает автоматический выбор размера региона, вы можете задать его вручную с помощью опции -XX:G1HeapRegionSize=?. Значение должно быть степенью двойки, если мерить в мегабайтах. Например, -XX:G1HeapRegionSize=16m.

При желании можно изменить порог заполненности кучи, при достижении которого инициируется выполнение цикла пометок и переход в режим смешанных сборок. Это делается опцией -XX:InitiatingHeapOccupancyPercent=?, принимающей значение в процентах. По умолчанию, этот порог равен 45%.

Если же вы решите залезть в дебри настроек G1 по-глубже, то можете включить дополнительные функции опциями -XX:+UnlockExperimentalVMOptions и -XX:+AggressiveOpts и поиграть с экспериментальными настройками.

Достоинства и недостатки


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

Расплатой за достоинства G1 являются ресурсы процессора, которые он использует для выполнения достаточно большой части своей работы параллельно с основной программой. В результате страдает пропускная способность приложения. Целевым значением пропускной способности по умолчанию для G1 является 90%. Для Parallel GC, например, это значение равно 99%. Это, конечно, не значит, что пропускная способность с G1 всегда будет почти на 10% меньше, но данную особенность следует всегда иметь в виду.


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

Ранее:
← Часть 2 — Сборщики Serial GC и Parallel GC
← Часть 1 — Введение
Share post

Similar posts

Comments 39

    0
    Исходя из опыта, какую комбинацию GC оптимально применять для batch-процессинга, когда по большому счету паузы не играют решающей роли, а объем данных большой?
      0
      Правило большого пальца для приложений, ориентированных на высокую пропускную способность и не чувствительных к паузам, это использование Parallel GC, у которого даже есть второе название — throughput collector. Для среднестатистического приложения при больших объемах данных задержки на очистку у Parallel GC могут быть весьма заметными, но общая пропускная способность наверняка будет выше, чем у CMS или G1. Но, как я уже говорил, каждое приложение в чем-то особенно, и подходить к оптимизации сборки мусора лучше со знанием того, как конкретно у вас устроена работа с данными. Иногда небольшая деталь в реализации программы может заставить полностью изменить подход к ее оптимизации. Об этом подробнее мы поговорим в следующей статье.
      0
      Спасибо за детальный обзор. Пара замечаний:

      — Название цикла очень невнятное. Когда времени мало и пробегаешь глазами по списку популярного за сутки, то большая вероятность, что часть релевантной аудитории просто пропустит такую статью.

      — В который раз буду спорить, что «конкурентный» — неверный перевод слова «concurrent».
        +2
        Название цикла очень невнятное.

        Между названием, которое принесет больше посетителей, и названием, которое мне нравится, я всегда выбираю второе. Я плохой smm-щик )

        «конкурентный» — неверный перевод слова «concurrent»

        Готов исправить в статье этот термин, если предложите более подходящий в данном конексте вариант.
          –3
          В данном контексте подходит «параллельный» на мой взгляд. Гугл предлагает «совместный».
            0
            Параллельный внесет полную путаницу с Parallel GC. Совместный не передает смысл отбирания у основного приложения ресурсов за счет параллельного выполнения. Я неспроста упомянул контекст.
              –1
              «Параллельный» как раз путаницу не внесет, ибо у Вас написано «конкурентный режим», а «параллельный режим» вполне себе без путаницы. Хотя наверно можно еще более аккуратно написать «режим параллельной сборки» или даже «режим сборки в несколько параллельных потоков».

              Но по сути это все уже мелочи жизни, главное все поняли, о чем идет речь.
                +1
                Позволю себе не согласиться. Я, как автор, не могу вывалить на читателей одинаковые термины, обозначающие разные вещи (причем критичные в данном контексте) и сказать, что это все мелочи жизни. Пока оставляю термин как есть.
                  –1
                  Убедили, что это разные вещи, но я теперь категорически не соглашусь с тем, что это «конкуренция за ресурсы процессора», хотя она тоже имеет место быть. Это конкуренция за возможность менять одни и те же области памяти и процессор здесь особо ни при чем.

                  Вот небольшое пояснение разницы Concurrency vs Parallelism
                    0
                    Мне кажется, текст по приведенной вами ссылке полностью подтверждает то, что я описал:
                    Threads which run concurrently do not always run simultaneously (eg. 2 threads on 1 core)… although to the end user they may well appear to – provided the cpu switches between them quickly enough.

                    Два потока на одном ядре, которое вынуждено между ними переключаться — это и есть конкуренция за ресурсы процессора.

                    Вы также можете открыть официальную документацию Oracle и обратить внимание на использование слова concurrent в ней. Особенно на секцию Concurrent Phases, в которой, как можно понять из названия, речь как раз об этой особенности сборщика. В ней всего несколько предложений и все они про одно и то же — конкуренцию потоков сборщика с потоками основной программы за ресурсы процессора, в результате которой уменьшается общая пропускная способность. Об изменении памяти там ни слова.

                    И чтобы два раза по ссылке не ходить, поищите сразу там же использование слова parallel. Оно всегда используется исключительно для обозначения параллельного сборщика. Чтобы не вносить путаницу. Я тоже стараюсь не вносить путаницу. И дополнительно всегда стараюсь приводить оригинальные английские термины, чтобы при необходимости можно было разобраться, о чем речь.
            0
            Я плохой smm-щик )

            SMS знаю, MMM знаю, SMM — не знаю )

            Готов исправить в статье этот термин, если предложите более подходящий в данном конексте вариант.

            Как насчет «одновременный»? Насчет же конкуренции, параллельный сборщик (parallel) при прочих равных будет конкурировать с основными потоками еще больше, чем ваш конкурентный, потому что у параллельного больше своих потоков. Поэтому, с вашей точки зрения, это параллельный сборщик надо называть конкурентным. :)
              0
              ОК, давайте разберем еще раз всё подробно. Выше в комментариях приведена ссылка на статью Concurrent != Parallel, в которой как раз объясняется разница между терминами «параллельный» и «конкурентный», и содержится призыв не путать эти понятия. Вот основная идея: Параллельный — это реально выполняющийся параллельно, без конкуренции за какие-либо ресурсы, приводящей к необходимости приостановок выполнения. Пример параллельного выполнения — N потоков на N ядрах процессора без общей памяти. Конкурентный — это выполняемый как-бы параллельно, но из-за конкуренции за ресурсы на самом деле никакого реального параллельного выполнения нет. Пример конкурентного выполнения — два потока на одном ядре процессора, или N потоков, пишущих в одну и ту же область памяти.

              Теперь давайте взглянем на наши сборщики, в которых два этих понятия используются абсолютно правильно:

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

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

              То есть использование названия concurrent и его перевода как «конкурентный» оправдано для CMS/G1 и не может иметь отношения к Parallel GC. При этом использование названия parallel и его перевода как «параллельный» абсолютно оправдано для Parallel GC и не может относиться с CMS/G1.

              В свете вышесказанного считаю, что «конкурентный» подходит больше, чем «одновременный».

              SMM — Social Media Marketing.
                0
                Намеренно или случайно, но лукавите ;)

                Согласен, что я упустил эту «деталь» параллельного сборщика, который вообще останавливает рабочие потоки. Это очень важно.

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

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

                  Сборщик просыпается, видит кучу мусора и не торопясь, в одном потоке, его собирает, после чего засыпает. Как его назовем? Пусть будет Serial GC, ведь всё последовательно и нужно с чего-то начать.

                  Проходят годы, появляется второй тип сборщика. Он просыпается, видит кучу мусора и разбирает ее, но уже используя несколько потоков, работающих параллельно. Как назовем такой сборщик? Логично назвать Parallel GC, нет?

                  Предыдущий сборщик работает быстро, но даже его короткие паузы устраивают не все приложения, и появляется новый тип сборщика, который во время некоторых фаз своего бодрствования обнаруживает, что, оказывается, кроме него есть еще и другие потоки (приложения), с которыми ему приходится работать в конкурентом режиме. Что это за фазы такие? Это фазы Mark и Sweep. Как назовем такой сборщик, у которого фазы Mark и Sweep проходят в конкурентном режиме? Не самым плохим вариантом будет Concurrent Mark Sweep. Длинно. Сократим до CMS.

                  Годы идут, объемы данных растут, требования к задержкам ужесточаются, но и эволюция не стоит на месте. Появляется сборщик, который, помимо того, что имеет конкурентные фазы, еще и может выбирать, какую часть вверенного ему пространства памяти убирать, а какую оставить на потом. И он, как мудрый сборщик, решает в первую очередь убирать то, что больше всего замусорено. Как назовем такой сборщик? Давайте Garbage First, вроде не самый плохой вариант. Но тоже длинно, сократим до GF? Нее, все будут смеяться, что это girlfriend. Над G1 так не посмеешься, да и выглядит современнее, остановимся на этом варианте.

                  Два последних сборщика, в отличие от первых, имеют фазы конкурентного выполнения. Как бы их назвать собирательно? Конкурентные. Не потому что они так всегда взаимодействуют с основным приложением. А потому что у них есть конкурентные фазы, а у других нет, считаете не логично? Вы ведь протягивая 50 рублей продавщице мороженого и говоря «мне ореховое» не ожидаете (надеюсь), что она вам большой орех на палочке даст. И правильно, она вам его не даст, а даст мороженое с кусочками орехов.

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

                  А предложение перевести Parallel GC как «многопоточный останавливающий» с одновременным упреком в путанице и в отсутствии логики в исходных названиях вряд ли можно рассматривать всерьез. Останавливают выполнение потоков приложения все (все!) сборщики, поэтому и раздел «Ситуации STW» есть в описании каждого из них. Многопоточными являются три из четырех. «Однопоточный останавливающий», «многопоточный останавливающий без конкурентного выполнения», «многопоточный останавливающий с конкурентным выполнением фаз Mark и Sweep», «многопоточный останавливающий с конкурентными фазами и дробными поколениями». Так будет звучать ваше предложение о переводе терминов Serial / Parallel / CMS / G1 полностью?

                  А с таким подходом к выискиванию несоответствий вы можете бесконечно спорить с кем угодно и о чем угодно. Начать можно вообще с самого термина «garbage collector». Собиратель? Коллекционер? Коллектор? Да мы вообще мусор никуда не собираем и не коллекционируем, мы его просто удаляем, поэтому давайте не будем запутывать картину: garbage deleter! Ах да, если уж разбираться всерьез, то на самом деле мы его даже не удаляем, а просто оставляем где был и игнорируем. Может, garbage ignorer?

                  И так вы можете до бесконечности. А я, пожалуй, воздержусь до появления реальных, полностью продуманных предложений.
                    –3
                    Ваши описания типов сборщиков упускают важную черту: когда именно сборщик останавливает приложение. Первые два как будто вообще не ведают, что останавливают все остальные потоки. Третий тоже якобы не ведает, что потоки не останавливает, и «вдруг» как бы прозревает — ба, да тут и другие потоки есть!
                    Однако, если в аудитории, не знающей про детали реализации сборщиков, спросисть: «конкурирующий сборщик — это такой, который конкурирует с потоками проложения. Вопрос: а что такое параллельный сборщик?» То ответ будет однозначным: «это такой сборщик, которой не конкурирует, а работает параллельно».
                    В английском варианте, «одновременный» и «параллельный», чувствуется некая похожесть терминов, поэтому заранее понятно, что есть подвох и особой логики тут искать не стоит, а надо разобраться и просто запомнить названия. В вашем варианте ложная логика явно присутствует.

                    Кстати, кто сказал, что параллельный сборщек не конкурирует сам с собой? В системе обычно сотни и тысячи активных потоков. Ну будет сотни + потоки сборщика.

                    Ну и про пример с мороженным, он не полный. Полный вот такой:
                    — Дайте мне ореховое. Дают ореховое.
                    — Дайте мне банановое. Дают банан.
                    Почему? А потому что. ;)
                      +1
                      Вы почему-то продолжаете адресовывать претензии по выбору исходных именований сборщиков именно мне. Не я выбирал термины Parallel и Concurrent для них. Вам они могут не нравиться, и вы можете настаивать на их переименовании. Но логичнее это делать не здесь, а на форуме Oracle.

                      Я же объяснил логику, скрытую за этими терминами, и выбрал для них соответствующие этой логике переводы. Если вы сомневаетесь, что я правильно понял, что имелось в виду под терминами «parallel» и «concurrent» в именах сборщиков, то приглашаю вас почитать официальную документацию Oracle, обращая внимание на использование этих терминов. Они используются ровно так, ровно в том виде и ровно в том значении, что я описал.

                      И никаких реальных, более корректных альтернатив вы опять не предлагаете. Честно говоря, не вижу смысла продолжать диалог в таком русле.

                      Пример с мороженым, кстати, всего лишь доказывает право на существование такого подхода к именованию, а не его единственность и обязательность его использования везде и всегда. Так что ваше дополнение некорректно.
                        –1
                        Моу сухой остаток:
                        Parallel и Concurrent — почти одинаковые термины, часто взаимозаменяются, поэтому тут понятно, что они обознают не различия между подходами к сборке мусора, а просто некие особености. Они, можно сказать, почти имена собственные в данном случае.
                        Конкурентный и параллельный — совсем не одинаковые термины, во многом даже противоположные, если используются парой, поэтому ошибочно заставляют думать, что они определяют главное различие между разными сборщиками.
                        Я предложил «одновременный». Согласен, не очень удачный термин, но он хотя бы сохраняет исходную сожесть между concurrent и parallel.
          +1
          Неплохая статья. Небольшие ремарки:
          он старается разносить во времени малые и старшие сборки мусора, чтобы они совместно не создавали продолжительных пауз в работе приложения
          На самом деле, ровно наоборот. Перед запуском CMS цикла JVM по возможности дожидается сборки в молодом поколении (-XX:CMSWaitDuration), чтобы уменьшить время Initial mark. Аналогично перед remark фазой зачастую просят CMS опять дождаться сборки в молодом поколении (-XX:+CMSScavengeBeforeRemark), чтобы уменьшить длительность remark.

          серьезно рассматривают его на роль сборщика по умолчанию
          Уже сделали.

          -XX:AggressiveOpts
          Не надо советовать эту опцию. Во-первых, она не имеет отношения к GC, во-вторых, не предназначена для использования в production, и порой даже вредна.
            0
            На самом деле, ровно наоборот.

            Я бы не сказал, что ровно наоборот. На самом деле, ситуация просто сложнее: CMS пытается как можно дальше разнести по времени паузы remark (так как она большая) и паузу малой сборки. С паузой initial mark он этого делать, действительно, не пытается. Так, по крайней мере, утверждает официальная документация Oracle: Concurrent Mark Sweep Collector (секция Scheduling Pauses). И да, согласен, чем ближе initial mark к последней малой сборке, тем эффективнее будет последующая большая сборка.

            Уже сделали.

            Ну, JDK 9 еще не выпущен. А в соответствующей задаче они оставляют себе место для отступления.

            Не надо советовать эту опцию.

            Я не советовал, я сообщил о ее наличии и предупредил, что это экспериментальная функциональность. Но иногда ее советуют даже сами разработчики из Oracle (см. ссылку с примером Solr) и она используется в пром. средах. Конечно, согласен, что это исключение из правил, и эта опция определенно может быть вредна. Но в некоторых случаях вредной может быть практически любая из перечисленных опций. Я все же полагаюсь на разумность читателей )
            0
            Наблюдал работу G1 под нагрузкой. По сравнению с ParallelGC просадка 30-40%. Зависит, конечно, от конкретной системы, но я бы пользоваться G1 без сравнительного тестирования не рекомендовал.
              0
              Просадка по какому параметру? Время отклика или пропускная способность?
                0
                По пропускной способности. Времена отклика тоже были хуже, и, что самое неприятное, были нестабильны.
                  0
                  А какой объем кучи? И сколько ядер процессора было доступно приложению?
                    0
                    Размер кучи 10 Гб. 24 логических ядра на виртуальной машине, если память мне не изменяет.
                      0
                      Нестабильность времени отклика обычно возрастает когда слишком часто выполняются полные (в случае G1 смешанные) сборки. Возможно, у вас слишком много мусорных данных просачивается в старшее поколение. Либо возникают evacuation fault'ы. И то и другое грубо можно проверить хотя бы с помощью -verbose:gc. И на загрузку ядер посмотреть. Если они полностью загружены самим приложением, то конкурентные сборщики (CMS и G1) могут терять эффективность. Я в следующей статье-двух планирую описать множество различных аспектов, влияющих на поведение сборщиков, с примерами настройки. Надеюсь, вам удастся найти там полезные идеи для оптимизации сборки в вашем приложении, потому что G1 все-таки обычно обеспечивает лучшее время отклика, если в приложении нет какой-нибудь серьезной специфики. А пропускную способность он, действительно, просаживает, таков его принцип работы.
                        0
                        Размер кучи был 6 Гб. gc.log в наличии. Подскажите, что в нем посмотреть?

                        Меня вот такие записи смутили

                        [Full GC 2171M->671M(6144M), 4.1407616 secs]
                        [Eden: 290.0M(496.0M)->0.0B(814.0M) Survivors: 40.0M->0.0B Heap: 2171.3M(6144.0M)->671.9M(6144.0M)], [Perm: 524287K->252914K
                        (253952K)]
                        [Times: user=5.05 sys=0.42, real=4.13 secs]
                          0
                          Я правильно понял, что это 7-я джава?
                          Сборку нужно в динамике наблюдать. По одному этому примеру видно только, что в старшем поколении у вас, действительно, очень много мусорных данных (освобождено ~1.5 ГБ за сборку, из которых больше гигабайта из старшего поколения). Если вы такую же картину наблюдаете во всех сборках, то нужно перенастраивать распределение кучи между младшим и старшим поколениями. Хотя по этому отрывку лога видно, что G1 уже сам начал это делать, увеличив размер младшего поколения. Скорее всего, следующая сборка будет уже лучше. Как я сказал, нужно в динамике наблюдать и дать время сборщику подстроиться под ваше приложение.
                            0
                            Хотя это больше похоже на Parallel GC, чем на G1. Какую версию JRE вы используете?
                              0
                              Понятно.

                              А правильно ли я понимаю, что G1 во время малой сборки приостанавливает отдельные потоки?
                                0
                                Да, малые сборки во всех сборщиках, включая G1, выполняются при остановленных потоках основного приложения. Ответьте, пожалуйста, про версию JRE. Если это 7-ка, как я предположил, то G1 в ней может быть еще достаточно сырым. В 8-й версии он заметно лучше.
                                  0
                                  Так и есть. Java 7.

                                  Тогда вобщем-то понятно из-за чего происходило торможение. GC останавливает поток, работающий в synchronized-методе, а за ним останавливаются и многие другие потоки на входе в этот метод. В результате получается, что каждая малая сборка работает как stop-the-world.
                                    0
                                    Малая сборка — это всегда STW, независимо от сборщика. Она останавливает все потоки приложения, так что с этой точки зрения наличие у вас синхронизации никак не влияет на паузы. Я не знаю, на каком этапе работы вашего приложения была выполнена указанная выше сборка, но если в вашем приложении есть постоянные данные, которые к этому моменту уже были полностью начитаны, то вам можно попробовать явно задать большое значение для размера младшего поколения. В вашем случае видно, что в старшем поколении после сборки осталось чуть меньше 700 МБ. Возможно, это и есть ваши постоянные данные и больше их не будет. Если так, то можете попробовать установить -XX:MaxNewSize=5400m, чтобы явно отдать почти все место, не занятое постоянными данными, под младшее поколение. А если ваше приложение обрабатывает множество относительно небольших запросов, и вы хотели бы уменьшить время отклика на них, то, возможно, 6ГБ для него слишком много. Чем больше куча, тем дольше она чистится, часто уменьшение кучи приводит к положительным результатам в транзакционных приложениях. Попробуйте в этом случае уменьшить Xms и Xmx, одновременно уменьшив указанное выше значение MaxNewSize на такую же величину.

                                    Но имейте в виду, что тут может быть очень много нюансов. Возможно, в указанной сборке учитываются не все постоянные объекты. Возможно, у вас есть долгоиграющие методы, которым нужен такой большой объем памяти, поэтому его нельзя уменьшать. Возможно, у вас периодически создаются большие объекты (больше нескольких мегабайт), которые приходится размещать в Tenured, минуя Eden. И еще много чего может сыграть свою роль. Тут в комментарии я все не смогу перечислить подробно. Постараюсь как можно больше описать в следующей статье.
                                      0
                                      Спасибо.
                0
                Удалено
                  +1
                  >он не дефрагментирует память.

                  может наоброт — не фрагментирует? дефрагментация — процесс устранения фрагментации 8)
                  аналогичная ошибка в предыдущей статье
                    0
                    Да, конечно, моя ошибка. Спасибо.
                    0
                    Что-то затянулось написание следующей статьи :)
                      0
                      Да, самого совесть грызет, но времени на хабростатьи пока что катастрофически не хватает. Но, конечно, нужно продолжить, тем более, что с момента этой публикации были интересные подвижки в мире GC для JVM.

                      Пойду пока хотя бы новые картинки придумаю для продолжения, глядишь и сдвинется дело :)

                    Only users with full accounts can post comments. Log in, please.