От переводчика: сейчас мы активно тестируем свой фреймворк Jmix на совместимость с JDK 17. Это последняя LTS версия, и рано или поздно на нее будет массовый переход (особенно после выхода Spring 6). Но есть ещё один аспект, который может сподвигнуть вас на переход на новый JDK - "бесплатное" увеличение производительности приложения. За счет чего? Узнаем в этой статье. JDK 17 с нами уже несколько месяцев и он не только про новые возможности языка Java. Прирост производительности в сравнении с более старыми версиями JDK очень заметен. Это становится особенно очевидно, когда мы сравниваем его с предыдущими LTS версиями: JDK 8 и JDK 11. Большая доля в улучшении производительности происходит из новых функций и оптимизаций в JVM, и в этой статье фокус будет на улучшениях, которые были сделаны в области сборки мусора в JVM Недавно у меня было выступление, посвященное новым возможностям в G1, начиная с JDK 8, и в этой статье мы расширим тему, чтобы также осветить прогресс, сделанный в Parallel GC и ZGC. Также у нас есть четвертый сборщик мусора, который мы поддерживаем, Serial GC, но он не будет включен в это сравнение. Serial – стабильный сборщик с небольшими накладными расходами, но тесты, которые мы будем использовать ниже, требуют высокопроизводительный сборщик мусора для нормальной работы. Решить, какой сборщик мусора использовать – не всегда простая задача. Важно понимать, что для правильного выбора сначала нужно понять, каковы у вас основные цели. Часто цели – это оптимизация для большей пропускной способности, уменьшения латентности и/или потребления памяти. Оптимальным решением, конечно же, было бы оптимизировать все эти метрики и получить наилучшую производительность в любой ситуации. Все сборщики мусора стараются быть оптимальными во всех аспектах, но они спроектированы с некоторыми компромиссами, чтобы поддерживать разные случаи, в которых они могут использоваться. Быстро пройдемся по тому, что мы имеем в виду, когда говорим о производительности в разных областях: Пропускная способность – снижает воздействие GC на общее количество транзакций, которое может быть выполнено в заданный промежуток времени Латентность – снижает воздействие GC на каждую единичную транзакцию Объем памяти – уменьшает потребление дополнительных ресурсов, используемых GC Когда мы говорим о компромиссах, это не значит, что сборщик не может быть улучшен во всех аспектах. В процессе улучшения сборщика мусора важной частью является то, чтобы все компромиссные части были сделаны настолько эффективно насколько можно. Ещё один хороший подход для глобального улучшения – пересмотреть решения, принятые при проектировании старой версии и найти лучшие решения для новой. Если взглянуть на прогресс, сделанный со времен JDK 8, то можно увидеть, что сборщики улучшились практически во всех областях. Чтобы лучше это показать, сравнительные диаграммы ниже используют нормализованные значения индивидуального сравнения сборщиков мусора вместо "чистых" цифр. Для сравнения сборщиков мусора я использовал SPECjbb® 2015 с размером кучи 16 GB. Это хорошо известный и стабильный тест, который фокусируется не только на производительности GC, так что результаты ещё и покажут улучшения во всей платформе Java. В тесте производительности есть несколько режимов выполнения, и он производит замеры обоих метрик: пропускной способности и латентности. Латентность измеряется как пропускная способность с ограничением по времени ответа. Для сравнения времени пауз я запускал тест с фиксированной нагрузкой в течение часа. В этом случае сборщики нагружены одинаково вне зависимости от их конечных значений метрик. Последнее замечание перед тем, как взглянем на графики: ZGC появился в JDK 11 (годен для промышленного использования с JDK 15), так что у нас есть только два набора данных для ZGC, в то время как у G1 и Parallel их три. Взглянув на значения метрики пропускной способности, мы увидим, что все сборщики мусора значительно улучшились в сравнении со своими предыдущими версиями. ZGC показал наибольший прогресс в этой области. У G1 и Parallel все ещё неплохие показатели "чистых" значений пропускной способности, но при увеличении объема кучи ZGC наверстывает отставание. Когда дело касается метрик, мы должны помнить, что мы замеряем не только производительность GC. Остальные части платформы Java, например, JIT компилятор, также вносят свою лепту в эти улучшения. С точки зрения латентности результаты улучшились ещё сильнее. Здесь можно видеть все преимущества от усилий, вложенных в то, чтобы сделать паузы GC ещё короче. Когда дело доходит до этой метрики, большое количество улучшений можно списать на те вещи, которые были улучшены в GC. G1 показывает наибольший прогресс в улучшении этой величины. ZGC также сильно улучшился в аспекте латентности. Наиболее впечатляющая часть незаметна на этой диаграмме, потому что тест замеряет латентности во всем приложении. ZGC делает свое дело настолько хорошо в области сокращения времени пауз JVM, что мы начали видеть другие вещи, влияющие на цифры латентности. Если мы вместо этого посмотрим, насколько уменьшилась длительность пауз, то можно увидеть просто экстраординарную работу, которую делает ZGC. Здесь мы смотрим на "чистые" цифры (потому что нормализованная длительность пауз – это немного странно) и видим, что ZGC в JDK 17 сильно перевыполняет свои же планы по удержанию длительности пауз меньше миллисекунды. G1, который пытается держать баланс между латентностью и пропускной способностью, также показывает результаты лучше планируемой величины паузы в 200 миллисекунд. В эту диаграмму также включен ещё один дополнительный столбец, чтобы быстро показать, как разные сборщики мусора работают с увеличенным объемом кучи. ZGC был спроектирован так, чтобы размер кучи не влиял на длительность пауз, и мы ясно видим, что это работает, если увеличить кучу до 128 Гб. G1 работает с большой кучей лучше, чем Parallel с точки зрения длительности пауз, потому что в него заложена логика удержания целевого значения времени паузы. На этой диаграмме показано сравнение пикового значения потребления памяти для трех разных сборщиков мусора. Так как и ZGC, и Parallel довольно стабильны с этой точки зрения, есть смысл взглянуть на чистые цифры и в этом случае. Можно видеть, что G1 сильно улучшился в этой области и основная причина этого – все функции и улучшения, чтобы сделать управление ссылками между регионами памяти (remembered sets) более эффективным. Даже если остальные сборщики мусора и не уменьшили объем накладных расходов, мы все ещё должны помнить, что они улучшили другие показатели без использования дополнительных объемов памяти. Общая производительность JDK 17 в сравнении с предыдущими версиями значительно выросла вне зависимости от того, какой сборщик мусора вы используете. Если вы все еще сидите на JDK 8 и планируете обновляться, то самое время пересмотреть планы на то, какой сборщик мусора использовать. В JDK 8 сборщик Parallel был установлен по умолчанию, но был заменен на G1, начиная с JDK 9. Хотя G1 был значительно улучшен по сравнению с Parallel, все ещё существуют ситуации, когда Parallel является наилучшим выбором. С появлением ZGC (готов к промышленному применению, начиная с JDK 15), у нас появляется третья, высокопроизводительная величина, которую также нужно включить в уравнение. Если вам нужно больше подробностей по поводу того, что было сделано для достижения таких отличных результатов, я рекомендую к прочтению блог Per Linden’а, который посвящен ZGC и блог Thomas Schaltzl’а, посвященный G1 (и немного Parallel). Если вам интересны новости и мысли команды Java в Oracle, посмотрите на inside.java.