Low-latency оптимизации JVM

Данная статья является частью более общей статьи об оптимизациях торговой системы на всех уровнях. В этой статье даются советы по настройке одного из уровней low-latency приложения — уровня JVM.

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

Предварительные замечания

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

Каждая новая версия Java приносит новые параметры и удаляет другие. Каждая новая версия или билд Java может влиять на производительность в лучшую или худшую сторону. Так что при каждом обновлении версии все замеры производительности надо производить заново.

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

Все приведенное ниже не гарантирует, что в конкретно в вашей ситуации ваше приложение будет работать быстрее. Не следует надеяться, что криво написанное приложение заработает автоматически быстрее при включении тех или иных «волшебных» параметров JVM или настроек ОС. Для достижения low-latency само приложение должно писаться с учетом low-latency.

Параметры приведены для Java 8 HotSpot JVM.

Все настройки JVM можно разбить условно на четыре группы:

  • настройки heap
  • настройки сборщика мусора
  • настройки JIT-компилятора
  • прочие настройки

Обычные параметры

Параметры в этом разделе не оптимизируют ничего, я их использую для контроля и проверок:

-showversion — покажет в логах версию JVM при старте и продолжит старт системы. Полезно иметь эту информацию, чтобы в случае чего обнаружить например, что кто-то без вашего ведома поменял путь к Java или обновил версию.

-XX:+PrintFlagsFinal — распечатывает в логах окончательные значения всех флагов JVM, выставленные как по умолчанию, так и вами через командную строку. Полезно иметь эту информацию, чтобы проверять, не поменялись ли стартовые параметры без вашего ведома.

-XX:+PrintCommandLineFlags — распечатывает в логах список стартовых параметров из вашей командной строки. Тоже полезно вести учет.

Настройка Heap

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

-Xmx=26g -Xms=26g — выставляем минимальный размер равным максимальному, чтобы JVM сразу аллоцировала именно столько памяти, сколько по нашему мнению ей понадобится за все время работы приложения, и больше не тратила время (вызывает задержки) на постепенное увеличение heap. Размер heap вычисляется эмпирическим путем. Конечно обидно будет, если вы не угадали с размером: вылетит OutOfMemoryException, но уж постарайтесь сделать так, чтобы угадать.

Есть соблазн выделить несколько сот гигабайт памяти под heap, чтоб уж наверняка. Но, во-первых, чем больше heap, тем больше времени тратит JVM на отслеживание объектов по нему; во-вторых, если взять больше половины доступной физической памяти, некоторая часть памяти аллоцируется во втором, дальнем банке памяти, доступ к которому с вашего процессора будет медленнее (опять задержки); в-третьих, в JVM встроена функция compressed pointers, когда при определенном размере heap используются указатели меньшего размера, что немножко быстрее, чем при использовании полноценных 64-битных указателей. Документация говорит, что для включения этой функции размер heap-а не должен превышать 31 Гигабайт. Практика показывает, что compressed pointers включаются, если аллоцирован heap до 29 Гигабайт. Так как для моего конкретного случая даже 29 Гигабайт было достаточно, я выбрал значение для heap 26 Гигабайт.

-XX:NewSize=<> -XX:MaxNewSize=<> — выставляет начальный размер young generation равным максимальному, чтобы JVM не тратило время на изменение размера young generation.

-XX:MetaspaceSize=<> -XX:MaxMetaSpaceSize=<> — (помните, что Java 8 больше нет Perm Generation?) выставляем начальный размер мета-области равной максимальному размеру с той же целью.

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

Для понимания данного раздела от вас требуется знание, как работает в JVM сборщик мусора и какие сборщики мусора поставляются сейчас в JVM HotSpot.

-XX:+PrintGCDetails — распечатывает в логах информацию о сборке мусора. Так как наша цель — избегать всеми средствами сборки мусора во время активной фазы работы приложения, в логах никаких записей появиться не должно. Но если сборка мусора все же произошла, полезно знать, когда именно, чем вызвана и сколько мусора было собрано.

-XX:+PrintGCDateStamps — сопровождает логи календарной датой и временем, что позволяет привязать события в логах к конкретному событию или поведению JVM в определенное время. Опция -XX:+PrintGCTimeStamps дает время в миллисекундах, истекшее с момента старта JVM, но пользы от этого времени не вижу никакого.

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

Настройка JIT-компилятора

Для понимания данного раздела вы должны знать, что такое JIT-компилятор в JVM и как он работает.

-XX:-TieredCompilation — здесь по вкусу. Можно отключить ступенчатую компиляцию и сразу агрессивно компилировать код компилятором C2, а можно оставить включенной и дать приложению прогреться как следует, собрать статистику по исполняемому коду и получить более качественный результат.

-XX:TieredStopAtLevel=<> (1, 2, 3) — бывают случаи, когда сколько бы вы не прогревали код, C2 компилятор постоянно его перекомпилирует. Ну нет у вас, скажем, времени сейчас выяснять, почему. Можно приказать JVM остановиться на определённом уровне компиляции, скажем на C1 Level 1.  Код будет работать чуть медленнее с меньшим количеством оптимизаций, но зато без задержек, вызванных постоянной перекомпиляцией кода.

-XX:CompileThreshold=<> — задает количество исполнений метода, после которого он будет считаться «горячим» и станет кандидатом на JIT-компиляцию.

-XX:+PrintCompilation -XX:+PrintInlining — выводят в логи информацию о компиляции кода, декомпиляции и оптимизациях. Используем при разработке и проверке приложения. После прогрева никаких компиляций больше мы не ждем. Значит, если после прогрева мы видим какие-то сообщения о компиляции, проверяем код прогрева и добиваемся, чтобы компиляция произошла именно во время прогрева кода.

-XX:+LogCompilation — (требует -XX:+UnlockDiagnosticVMOptions). Выводит в log-файл XML-информацию о компиляциях, которую затем можно распарсить и посмотреть например приложением JITWatch.

-XX:CICompilerCount=2 — создает только два потока. Одни поток используется для C1 компилятора, второй — для C2.

Прочие параметры

-XX:+TraceClassLoading — выводит в лог, какие классы в какой момент загружаются вашим приложением. После правильного прогрева системы все требуемые классы должны быть загружены. Догрузка очередного класса в самый неподходящий момент, во-первых, вызовет задержку (на загрузку класса и его инициализацию требуется время); во-вторых, приведет к декомпилляции уже прогретого кода, и он снова будет исполняться в режиме интерпретации (т.е. медленно), пока не будет заново перекомиллирован (опять будет задержка на вызов JIT-компиллятора и замену на лету байт-кода на машинный).

-XX:-ClassUnloading — запрещает выгружать уже загруженные ненужные классы, чтобы освободить место для загрузки новых классов. Если при старте и прогреве все нужные классы загружены, новые классы при исполнении загружаться не будут, значит и новое место не понадобится. Значит можно отключить выгрузку классов и тогда JVM не будет тратить время на выполнение этой функции.

-XX:-UseCodeCacheFlushing — запрещает сбрасывать кэш уже откомпиллированного кода. Если JVM разрешить сбрасывать кэш, когда он заполнен, JVM может это сделать в самый неподходящий момент, что вызовет задержки. Если запретить сбрасывание, а кэш — заполнен, старый код не будет сбрасываться из кэша, компиляция нового кода остановится, т.е. JIT-компилятор прекратит свою работу. Обычно размера кэша хватает. Размер кэша можно устанавливать другими параметрами: -XX:InitialCodeCacheSize и -XX:ReservedCodeCacheSize.

-XX:+UseNUMA — практически обязательный флаг, если ваша программа работает на многопроцессорной системе и потоки вашего приложения распределены по нескольким процессорам.

-XX:-UseBiasedLocking — отключает использование biased locking. Biased locking — оптимизация, которая подходит для обычных Java-программ, но в low-latency эта оптимизация неэффективна и приводит к неприятным задержкам. Начиная с Java 15 biased locking отключен по умолчанию. Скорей всего ее выпилят в следующих версиях Java как неэффективную и не оправдавшую надежд.

Итоги

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

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s