Разочаровывающая история с оптимизацией памяти
Не все истории должны быть историями успеха. Реальность тоже не такая. Мы хотели бы поделиться правдивой, разочаровывающей историей (но феноменальным опытом обучения), которая может быть вам полезна.
Это история об оптимизации использования памяти веб-приложением. Это приложение было настроено с большим объемом памяти (4 ГБ) только для обслуживания нескольких транзакций в секунду. Таким образом, мы приступили к изучению моделей использования памяти этим приложением. Мы захватили дампы кучи этого приложения с помощью инструмента jmap. Мы загрузили захваченный дамп кучи в инструмент HeapHero. HeapHero — это инструмент для анализа дампа кучи, такой же, как Eclipse MAT, JProfiler, Yourkit. Инструмент HeapHero профилировал память и предоставлял статистику по общим классам, общим объектам, размеру кучи, представлению гистограмм больших объектов, находящихся в памяти. Помимо этих традиционных показателей, HeapHero сообщал об общем объеме памяти, потраченной впустую из-за неэффективных методов программирования. В современных вычислениях значительный объем памяти тратится впустую из-за неэффективных методов программирования, таких как: создание дубликатов объектов, неоптимальные определения типов данных (объявление «двойных» и присвоение только значений «плавающих»), чрезмерное выделение и недостаточное использование структур данных и некоторые другие методы. .
Это приложение не стало исключением. HeapHero сообщил, что приложение тратит 56% памяти впустую из-за неэффективных методов программирования. Да, брови поднимаются на 56%. Он сообщил, что 30% памяти приложения тратится впустую из-за повторяющихся строк.
Рис: Инструмент HeapHero отчет о количестве памяти, потраченной впустую из-за неэффективного программирования
Дедупликация строк
Начиная с Java 8, обновление 20, был введен новый аргумент JVM «-XX: + UseStringDeduplication». Когда приложение запускается с этим аргументом, JVM удалит повторяющиеся строки из памяти приложения во время сборки мусора. Однако имейте в виду, что аргумент «-XX:+UseStringDeduplication» будет работать только с алгоритмом G1 GC. Вы можете активировать алгоритм G1 GC, передав ‘-XX:+UseG1GC’.
Мы заволновались. Мы думали, что просто введя аргумент JVM ‘-XX:+UseG1GC -XX:+UseStringDeduplication’, мы сможем сэкономить 30% памяти без какого-либо рефакторинга кода. Вау, разве это не прекрасно? Чтобы проверить эту теорию, мы провели два разных теста в нашей лаборатории производительности:
Тест 1: Передача ‘-XX:+UseG1GC’
Тест 2: Передача ‘-XX:+UseG1GC -XX:+UseStringDeduplication’
Мы включили журналы сбора мусора в приложении, чтобы изучить шаблон использования памяти. Проанализированы журналы сбора мусора с помощью бесплатного онлайн-инструмента анализа журнала сбора мусора — GCeasy. Мы надеялись, что в тестовом прогоне №2 мы сможем увидеть снижение потребления памяти на 30% из-за устранения повторяющихся строк. Однако реальность была совсем другой. Мы не увидели никакой разницы в использовании памяти. Оба тестовых прогона неизменно показывали одинаковый объем использования памяти. Посмотрите графики использования кучи, созданные инструментом GCeasy, путем анализа журналов сборки мусора.
Рис. График использования GCeasy Heap с ‘-XX:+UseG1GC’
Рис. График использования кучи GCeasy с параметром «-XX:+UseG1GC -XX:+UseStringDeduplication»
In Test run #1 heap usage hovering around 1500mb all through the test, in test run #2 also heap usage was hovering around 1500mb. Disappointingly we didn’t see the anticipated 30% reduction in the memory usage, despite introducing ‘-XX:+UseG1GC -XX:+UseStringDeduplication’ JVM arguments.
Почему не было сокращения использования кучи?
«Почему не сократилось использование кучи?» – этот вопрос нас очень озадачил. Правильно ли мы настроили аргументы JVM? Разве ‘-XX:+UseStringDeduplication’ не выполняет свою работу правильно? Верен ли отчет об анализе инструмента GCeasy? Все эти вопросы тревожили наш сон. После детального анализа мы выяснили горькую правду. Видимо ‘-XX:+UseStringDeduplication’ устранит повторяющиеся строки, которые присутствуют только в старом поколении памяти ☹. Это не устранит повторяющиеся строки в молодом поколении. Память Java имеет 3 основных региона: молодое поколение, старое поколение и метапространство. Вновь созданные объекты переходят в молодое поколение. Объекты, просуществовавшие дольше, переводятся в старое поколение. Связанные с JVM объекты и метаданные хранятся в Metaspace. Таким образом, указание другими словами «-XX:+UseStringDeduplication» удалит только повторяющиеся строки, которые существуют в течение более длительного периода. Поскольку это веб-приложение, большинство строковых объектов были созданы и уничтожены сразу. Это было очень ясно из следующей статистики, представленной в отчете об анализе журнала GCeasy:
Рис. Статистика создания/продвижения объекта, представленная GCeasy
Средняя скорость создания объектов этого приложения: 44,93 Мб/сек, тогда как средняя скорость продвижения (т.е. от молодого поколения к старому) всего 918 Кб/сек. Показательно, что очень небольшой процент объектов являются долгоживущими. Даже в этих продвигаемых объектах со скоростью 918 кбит/с строковые объекты будут составлять меньшую часть. Таким образом, количество повторяющихся строк, удаленных с помощью ‘-XX:+UseStringDeduplication’, было очень незначительным. Таким образом, к сожалению, мы не увидели ожидаемого сокращения памяти.
Вывод
(а). ‘-XX:+UseStringDeduplication’ будет полезен, только если в приложении много долгоживущих повторяющихся строк. Это не дало бы плодотворных результатов для приложений, когда большинство объектов недолговечны. К сожалению, большинство современных веб-приложений, объекты микросервисных приложений недолговечны.
(б). Другой известный вариант, рекомендуемый в отрасли для устранения повторяющихся строк, — это использование функции String#intern(). Однако String#intern() не будет полезен для этого приложения. Потому что в String#intern() вы в конечном итоге создаете строковые объекты, а затем сразу же удаляете их. Если строка недолговечна по своей природе, вам не нужно выполнять этот шаг, так как обычный процесс сборки мусора удалит строки. Кроме того, String#intern() имеет возможность добавить (очень небольшую) задержку к транзакции и нагрузку на ЦП.
(с). Учитывая текущую ситуацию, лучший способ удалить повторяющиеся строки из приложения — это реорганизовать код, чтобы убедиться, что повторяющиеся строки даже не создаются. HeapHero указывает пути кода, где создается много дубликатов строк. Используя эти указатели, мы собираемся продолжить наше путешествие по рефакторингу кода, чтобы уменьшить потребление памяти.