Тюнинг JVM на примере одного проекта

в 12:16, , рубрики: hotspot, java, jpoint, jvm, Блог компании Luxoft, метки: , , ,

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

История проекта

Начиналось все прекрасно и безоблачно. Для нужд одного крупного банка было необходимо реализовать калькулятор, вычисляющий значение Value-at-Risk для конкретного инвестиционного портфеля. Как и большинство финансовых приложений методология не подразумевает “тяжелых” вычислений, но поток данных порой по истине огромен.

Проблемы обработки большого объема данных обычно решаются за счет двух известных типов масштабирования: вертикального и горизонтального. С вертикальным все обстояло достаточно приемлемо. В нашем распоряжении была машина о 16 ядрах, с 16 GB RAM, Red Hat’ом и Java’ой 1.6. На таком железе можно было достаточно хорошо развернуться что, собственно, мы и успешно делали на протяжении нескольких.

Все было прекрасно до того момента, как к нам не постучался заказчик и не сказал, что IT-инфраструктура пересмотрена и вместо 16x16 мы имеем 4x1-2:
Тюнинг JVM на примере одного проекта
Естественно, было увеличено требование времени работы приложения в несколько раз, но. Передать наши эмоции на тот момент было достаточно трудно, но речь, внезапно, стала сильно приправлена различными аллегориями, аллюзиями и сравнениями.

Первые попытки

Для начала поясню, что представляет из себя Value-at-Risk калькулятор. Это программа с большим количеством «простых» вычислений, пропускающая сквозь себя большой объем данных.

Ключи оптимизации, которые были наиболее полезны:

  • -server — крайне полезный ключ, JVM разворачиваем циклы, инлайнит многие функции и т.д.
  • Работа со строками: -XX:+UseCompressedStrings, -XX:+UseStringCache, -XX:+OptimizeStringConcat

Естественно, такой простой прием не дал желаемых результатов и мы продолжили копать в направлении дальнейших возможностей «ужатия» приложения. Первое, что было решено сделать это удалить ненужные кэши и оптимизировать существующие (те, которые дают 20% прироста производительности из 80%). Ненужные были быстро удалены, а для работы над оставшимися мы решили посмотреть на различные типы ссылок в Java.

Итак, у нас доступны следующие типы ссылок:

  • Hard/Strong
  • Soft
  • Weak
  • Phantom

Зависимости между ними выглядят примерно так:
Тюнинг JVM на примере одного проекта

Посмотрим, что же гарантирует спецификация по работе с этими типами ссылок.
Hard/Strong ссылки — это самые обычные ссылки, которые создаются, когда мы используем ключевое слово «new». Такая ссылка будет удалена, когда количество ссылок на созданный объект достигнет нуля. Мягкие ссылки (Soft) могут быть удалены в случае, если виртуальной машине недостаточно памяти для дальнейшей работы. Слабые ссылки (Weak) могут быть собраны в любой момент, если GC так решит. Фантомная ссылка (Phantom) — это специальный тип ссылок, который необходим для более гибкой файнализации объектов, чем классический finalize.

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

Рассмотрим, к примеру, то, как собираются Weak ссылки:
Тюнинг JVM на примере одного проекта

Видим, что у нас нет никакой гарантии того, что объект будет доступен все время и может быть удален в произвольный момент времени. Из-за этой специфики было решено перестроить внутренние, наиболее «тяжелые» кэши на Soft ссылки. Нами двигало примерно следующее утверждение: «Пусть объект живет в кэше как можно дольше, но в случае недостатка памяти мы сможем вычислить его заново, в силу того, что требования по времени работы были увеличены».

Результаты были существенны, но работы приложения в вожделенных 4ГБ не принесли.

Детальное исследование

Дальнейшие исследования проводились с помощью различных средств профилирования:

  • Стандартные средства JVM: -XX:+PrintGCDetails, -XX:+PrintGC, -XX:PrintReferenceGC, etc
  • MXBean
  • VisualVM

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

Во-первых, необходимо было уменьшить количество генерируемых объектов. Было замечено, что большинство данных имеют схожую структуру: «XXX1:XXX2:XXX3 и тд». Все паттерны типа «XXX» были заменены ссылками на объекты из пула, что дало существенное уменьшение количества создаваемых объектов (примерно в пять раз), а также освободило дополнительный объем драгоценной памяти.

Во-вторых, мы решили более детально поработать со стратегиями сборки мусора. Как мы знаем, у нас доступны следующие стратегии сборки мусора:

  • Serial
  • Parallel
  • Parallel compacting
  • Concurrent Mark-Sweep
  • G1 collector

G1 был нам недоступен в силу того, что использовалась шестая версия Java. Serial и Parallel мало чем отличаются и не очень хорошо показали себя на нашей задаче. Parallel compacting был и интересен за счет фазы, позволяющий уменьшить дефрагментацию данных. Concurrent Mark-Sweep был интересен за счет того, что позволял уменьшить время на фазу stop-the-world и также не допускал сильную фрагментацию.

После сравнения Parallel compacting и Concurrent Mark-Sweep коллекторов было решено остановиться на втором, что оказалось хорошим решением.

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

Полученный урок

  • Помогли ключи работы со строками: -XX:+UseCompressedStrings, -XX:+UseStringCache, -XX:+OptimizeStringConcat и сама специфика строковых данных
  • Уменьшение количества используемых объектов
  • Тонкая настройка JVM занимаем много времени, но результаты более чем оправдывают себя
  • Узнавайте требования как можно раньше! :)

Более развернутую версию проблемы и этапах решения можно будет послушать на приближающейся конференции JPoint, которая пройдет в Санкт-Петербурге.

Автор: Silf

Источник

Поделиться

* - обязательные к заполнению поля