В этой статье я хочу рассказать о своем опыте оптимизации приложений под память с использованием стандартных механизмов оптимизации JVM, таких как различные типы ссылок, стратегии сборки мусора и множество ключей, влияющих на сбор мусора.
Я уверен, что каждому из вас приходилось жонглировать параметрами для повышения производительности и никакой черной магии или рецепта нехватки памяти вы не найдете в статье, я просто хочу поделиться своим опытом.
История проекта
Все началось прекрасно и безоблачно.Для нужд одного крупного банка необходимо было реализовать калькулятор, рассчитывающий значение Value-at-Risk для конкретного инвестиционного портфеля.
Как и большинство финансовых приложений, методология не предполагает «тяжелых» расчетов, однако поток данных порой поистине огромен.
Задачи обработки больших объемов данных обычно решаются с помощью двух известных типов масштабирования: вертикального и горизонтального.
С вертикалью всё было вполне приемлемо.
В нашем распоряжении была машина с 16 ядрами, 16 ГБ оперативной памяти, Red Hat и Java 1.6. На таком железе можно было неплохо разработать, что мы, собственно, и успешно делали в течение нескольких месяцев.
Все было хорошо, пока заказчик не постучал к нам в дверь и не сказал, что ИТ-инфраструктура была пересмотрена и вместо 16х16 у нас стало 4х1-2:
Естественно, требования ко времени работы приложения были увеличены в несколько раз, но.
Передать наши эмоции в тот момент было довольно сложно, но речь вдруг стала сильно сдобрена различными аллегориями, аллюзиями и сравнениями.
Первые попытки
Сначала объясню, что это такое Стоимость под риском калькулятор.Это программа с множеством «простых» вычислений, пропускающая большой объем данных.
Ключи оптимизации, которые оказались наиболее полезными:
- -server — чрезвычайно полезный переключатель, мы разворачиваем циклы JVM, встраиваем множество функций и т. д.
- Работа со строками: -XX:+UseCompressedStrings, -XX:+UseStringCache, -XX:+OptimizeStringConcat
Первое, что было решено сделать, это удалить ненужные кеши и оптимизировать существующие (те, которые дают 20% прироста производительности в 80%).
Ненужные были быстро удалены, а для работы над оставшимися мы решили посмотреть разные типы ссылок в Java. Итак, нам доступны следующие типы ссылок:
- Твердый/Сильный
- Мягкий
- Слабый
- Фантом
Давайте посмотрим, что гарантирует спецификация для работы с этими типами ссылок.
Жесткие/сильные ссылки — это наиболее распространенные ссылки, которые создаются, когда мы используем ключевое слово «новое».
Такая ссылка будет удалена, когда количество ссылок на созданный объект достигнет нуля.
Мягкие ссылки можно удалить, если на виртуальной машине недостаточно памяти для дальнейшей работы.
Слабые звенья могут быть собраны в любое время, если так решит GC. Фантомная ссылка — это особый тип ссылки, который необходим для более гибкой финализации объектов, чем классическая финализация.
Жесткие и фантомные ссылки были сразу исключены из нашего рассмотрения в связи с тем, что они не обеспечивают требуемой функциональности и гибкости.
Хардовые не удаляются в нужный момент, но с финализацией всё было нормально.
Рассмотрим, например, как собираются слабые ссылки:
Мы видим, что у нас нет гарантии, что объект будет доступен постоянно и его можно будет удалить в любой момент. Из-за этой специфики было решено пересобрать внутренние, наиболее «тяжелые» кеши с Soft-линками.
Нами двигало что-то вроде следующего утверждения: «Пусть объект живет в кеше как можно дольше, но если памяти недостаточно, мы можем вычислить его заново, в связи с тем, что требования ко времени выполнения увеличены.
» Результаты были значительными, но в заветных 4Гб приложение не работало.
Детальное исследование
Дальнейшие исследования проводились с использованием различных инструментов профилирования:- Стандартные возможности JVM: -XX:+PrintGCDetails, -XX:+PrintGC, -XX:PrintReferenceGC и т. д.
- MXBean
- ВисуалВМ
Проведя несколько замечательных дней, наблюдая за цифрами и буквами, были сделаны следующие выводы: объектов создается очень много и старое поколение сильно перегружено.
Чтобы решить эту проблему, мы начали присматриваться к различным сборщикам мусора и методам работы с ними.
Во-первых, необходимо было сократить количество генерируемых объектов.
Было замечено, что большая часть данных имеет схожую структуру: «XXX1:XXX2:XXX3 и т.д.».
Все шаблоны типа «ХХХ» были заменены ссылками на объекты из пула, что значительно сократило количество создаваемых объектов (примерно в пять раз), а также высвободило дополнительную драгоценную память.
Во-вторых, мы решили более детально поработать со стратегиями сборки мусора.
Как мы знаем, у нас есть следующие стратегии сбора мусора:
- Серийный
- Параллельно
- Параллельное уплотнение
- Параллельная маркировка
- коллектор G1
Параллельное сжатие было интересно благодаря фазе, позволяющей уменьшить дефрагментацию данных.
Параллельная Mark-Sweep была интересна тем, что позволяла нам сократить время на этапе остановки мира, а также предотвращала серьезную фрагментацию.
После сравнения коллекторов параллельного уплотнения и Concurrent Mark-Sweep было решено остановиться на втором, что оказалось удачным решением.
После боевой проверки всех описанных выше методик приложение стало полностью совместимым с новыми требованиями и было успешно запущено в производство! Все вздохнули с облегчением!
Урок выучен
- Помогли клавиши для работы со строками: -XX:+UseCompressedStrings, -XX:+UseStringCache, -XX:+OptimizeStringConcat и сама специфика строковых данных
- Уменьшение количества используемых объектов
- Точная настройка JVM занимает много времени, но результат того стоит.
- Узнайте требования как можно раньше! :)
JPoint , который пройдет в Санкт-Петербурге.
Теги: #java #jvm #hotspot #gc Tuning #jpoint #java
-
Видео С Youtube Впервые Упомянуто В Суде
19 Oct, 24 -
Первый Ноутбук С Поддержкой Light Peak
19 Oct, 24 -
Знакомство С Kohana 3.0. Часть 4.
19 Oct, 24