Рекомендации по оптимизации потребления памяти в Java | Статья в журнале «Молодой ученый»

Отправьте статью сегодня! Журнал выйдет 26 октября, печатный экземпляр отправим 30 октября.

Опубликовать статью в журнале

Автор:

Рубрика: Информационные технологии

Опубликовано в Молодой учёный №24 (314) июнь 2020 г.

Дата публикации: 09.06.2020

Статья просмотрена: 2321 раз

Библиографическое описание:

Наливайко, А. С. Рекомендации по оптимизации потребления памяти в Java / А. С. Наливайко. — Текст : непосредственный // Молодой ученый. — 2020. — № 24 (314). — С. 59-63. — URL: https://moluch.ru/archive/314/71486/ (дата обращения: 16.10.2024).



В статье описаны часто используемые способы по оптимизации потребления памяти Java приложений. Стоит понимать, что эти рекомендации несут информационный характер, зависящие от конкретной ситуации; использовав в одном случае, нет полной гарантии, что сработает в другом.

Ключевые слова: java, JVM, память, сборщик мусора, CPU, выделение памяти, утечка памяти.

Миграция на последнюю версию языка

Oracle постоянно улучшает производительность языка программирования с каждой новой версией. Поэтому, как один из вариантов, можно попробовать изменить версию компиляции того или иного проекта. Эта рекомендация актуально только в том случае, если над проектом введется разработка; когда миграция не такая болезненная как для бизнеса, так и для программистов, разрабатывающих ПО.

Использование сторонних аллокаторов памяти

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

Часто используемыми библиотеками являются Jemalloc и TCMalloc. Jemalloc является оптимизированным вариантом реализации функций malloc, который призван решать проблемы с фрагментацией при выделения памяти в несколько потоков возникающие на однопроцессорных и многопроцессорных системах и оптимальной утилизации ресурсов CPU.

TCMalloc (Thread-Caching Malloc) является аналогом Jemalloc от компании Google. Работает он по такому же принципу, однако его кэш избавлен от блокировок и работает в привязке к ядрам CPU, но откатывается на модель кэширования в привязке к потокам в случае отсутствия необходимой функциональности в ядре ОС (привязка кэша к CPU работает только в свежих ядрах Linux) [2].

Использование GraalVM

GraalVM — это виртуальная машина Java и JDK, основанная на HotSpot/OpenJDK и написанная на Java. GraalVM поддерживает разные языки программирования и модели выполнения, такие как JIT-компиляция и AOT-компиляция [8].

«Graal» в GraalVM — это название компилятора. Первый, и самый простой способ использования Graal — это использовать его как Java JIT компилятор. Основными возможностями являются быстрое выполнение и уменьшения времени старта и потребления памяти. Graal написан на Java, а не на C++, как большинство остальных JIT компиляторов для Java.

Начиная с JDK 10 его можно включить с помощью параметра:

$ -XX:+UseJVMCICompiler

Twitter — компания, на сегодняшний день, которая использует Graal на «боевых» серверах [4], и они говорят, что для них это оправдано, в терминах экономии реальных денег. Twitter использует Graal для исполнения приложений, написанных на Scala — Graal работает на уровне JVM байткода, т. е. применим для любого JVM языка.

Это первый вариант использования GraalVM — просто замена JIT компилятора на лучшую версию для ваших существующих Java приложений. Сильные стороны платформы Java особенно явно проявляются при работе с долго выполняющимися процессами и пиковыми нагрузками. Короткоживущие процессы, напротив, страдают от долгого времени запуска и относительно большого использования памяти.

Команда native-image по-настоящему компилирует ваш Java код и Java библиотеки, которые вы используете, в полноценный машинный код. Для компонентов среды выполнения, таких как сборщик мусора, мы запускаем нашу собственную новую VM, которая называется SubstrateVM, которая, как и Graal, также написана на Java.

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

Alibaba одними из первых используют native-image. Как отмечают их разработчики, время на запуск сервиса удалось сократить в 20 раз (с 60 до 3 секунд), потребления памяти в 6 раз (с 128 до 21 мегабайта), а задержки при выполнении GC теперь не превышают более 100 миллисекунд. Ранее потребление и сборка всех микросервисов занимало порядка 100 гигабайт памяти и 4000 тысячи секунд на сборку проекта. В совокупности, им удалось получить четырехкратное улучшение показателей (до 20 гигабайт памяти и 1000 секунд на сборку проекта) [6].

При использовании native-image есть некоторые ограничения [10]. Обязательное условие: во время компиляции должны присутствовать все классы; также есть ограничения в области использования Reflection API. Зато присутствуют некоторые дополнительные преимущества перед базовой компиляцией: например, выполнение статических инициализаторов во время компиляции. Таким образом, уменьшается количество работы, выполняемой каждый раз, когда приложение загружается.

Это второе применение GraalVM — распространение и выполнение существующих Java программ, с быстрым стартом и меньшим расходом памяти. Этот способ устраняет такие проблемы с конфигурацией, как поиск нужного jar во время выполнения, а также позволяет создавать Docker образы меньшего размера. На самом деле, преимуществ у данной виртуальной машины гораздо больше, однако они затронуты в данной статье не будут, так как почти не влияют на оптимизацию производительности.

Одним из фреймворков, использующий преимущества GraalVM, является Quarkus. Разработчик фреймворка обещает очень высокую скорость запуска приложения и небольшой расход памяти [1]. Данные с сайта разработчика можно увидеть в таблицах 1 и 2.

Таблица 1

Время от старта приложения до первого ответа (в секундах)

Конфигурация

REST

REST + JPA

Quarkus + GraalVM

0.016

0.042

Quarkus + OpenJDK

0.943

2.033

Traditional Cloud Native Stack

4.3

9.5

Таблица 2

Потребление памяти (в мегабайтах)

Конфигурация

REST

REST+JPA

Quarkus + GraalVM

12

28

Quarkus + OpenJDK

73

145

Traditional Cloud Native Stack

136

209

Использование более подходящего сборщика мусора

Популярными сборщиками мусора являются ZGC, G1, Epsilon, Shenandoah.

ZGC — «ленивый» сборщик мусора; он не работает, но выделяет новую память, не перезапуская её. Однако так было до JDK 13. По предварительным анонсам, он должен был решить проблему подвисаний java приложений — заявленные паузы не должны превышать 100 мс даже на многогигабайтных кучах.

Включить можно с помощью:

$ -XX:+UnlockExperimentalVMOptions -XX:+UseZGC

G1 (Garbage First) — стандартный сборщик мусора, начиная с JDK 9. Он нацелен на системы с большим количеством памяти. С самого начала использовался однопоточный полный цикл GC. В следующей версии добавили многопоточность. Создателем выступает OpenJDK. Каждое обновление версии языка приносит улучшения; например, в JDK 12 научили возвращать неиспользуемую память из кучи в ОС. Так же, как и остальные сборщики мусора, G1 разбивает кучу на молодое и старое поколения. Сборка мусора происходит по большей части в молодом поколении, чтобы увеличить пропускную способность сборки, сборка в старом поколении происходит гораздо реже.

По умолчанию он уже включен, но если требуется, активировать его можно с помощью:

$ -XX:+UseG1GC

Epsilon — сборщик мусора, который обрабатывает выделение памяти, но не реализует какой-либо реальный механизм её восстановления памяти. Как только доступная куча Java будет исчерпана, JVM закроется. Обычно его используют для тестирования производительности, давления памяти, для чрезвычайно недолгой работы программы или где сборка мусора не предусмотрена (например, создание прошивки для умной техники; сам факт сборки мусора является ошибкой). В остальных случаях его возможности уступают другим сборщикам мусора.

Включить можно с помощью:

$ -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC

Shenandoah — алгоритм сборки мусора, цель которого — гарантировать низкое время отклика (нижний предел — 10–500 миллисекунд). Это уменьшает время паузы сборщика мусора при выполнении работы по очищению одновременно с работающими потоками. У данного алгоритмы время паузы не зависит от размера кучи: будь heap размером 200 или 2 гигабайта, время паузы будет одинаковым. Является экспериментальной функцией, которая не включена в стандартную сборку OpenJDK.

Включить можно с помощью:

$ -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC

Тонкая настройка JVM

Настроить JVM для эффективного использования доступной оперативной памяти непросто. Если запустить JVM с параметром –Xmx16M и ожидать, что будет использоваться не более 16 МБ памяти, то это не так.

Интересной областью памяти JVM является кэш кода JIT. По умолчанию HotSpot JVM будет использовать до 240 МБ. Если кэш кода слишком мал, в JIT может не хватить места для хранения своих данных, и в результате будет снижена производительность. Если кэш слишком велик, то память может быть потрачена впустую. При определении размера кэша важно учитывать его влияние как на использование памяти, так и на производительность.

В 32-х разрядных системах размер указателя на ячейку памяти занимает 32 бита. Следовательно, максимально доступная память, которую могут использовать 32-х битные указатели — 232 = 4294967296 байт или 4 ГБ. В 64-х разрядных системах соответственно можно ссылаться на 264 объектов. Такое огромное количество указателей излишне. Поэтому появилась опция сжатия ссылок:

$ -XX:+UseCompressedOops

Эта опция позволила уменьшить размер указателя в 64-х разрядных JVM до 32 бит.

Оптимизация программного кода

Все вышеперечисленные советы будут полезны только в том случае, если программный код принудительно не вызывает утечки памяти. Первый сценарий, который может вызвать утечку памяти — это интенсивное использование статических переменных. В Java время жизни статических полей обычно совпадает со временем работы приложения. Например, метод, наполняющий статическую коллекцию (ArrayList) объектами, при завершении не очистит сборщиком мусора память (рисунок 1).

Рис. 1. Потребление памяти при наполнении статической коллекции

Однако, если мы отбросим слово static у переменной, то это приведет к резкому изменению использования памяти (рисунок 2).

Рис. 2. Потребление памяти при наполнении обычной коллекции

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

Вторым из сценариев можно рассмотреть ошибку некорректного написания переопределяемых методов equals() и hashCode(). Коллекции HashSet и HashMap используют эти методы во многих операциях и если они не переопределены правильно, то эти методы могут стать источником потенциальных проблем, связанных с утечкой памяти.

Рассмотрим пример наполнения коллекции HashMap простым классом Person (конструктор, переменные name и age) в качестве ключа. Как известно, Map не позволяет использовать дубликаты ключей, многочисленные объекты Person, которые мы добавили, не должны увеличить занимаемую ими пространство в памяти. Поскольку изначально не был переопределен правильный метод equals(), дублирующие объекты накопились и заняли память. В этом случае потребление памяти кучи выглядит следующим образом (рисунок 3).

Рис. 3. Потребление памяти объектов без переопределенных методов equals() и hashCode()

Если правильно переопределить методы equals() и hashCode(), тогда в Map будет существовать только один объект Person, и лишняя память не будет использоваться (рисунок 4).

Рис. 4. Потребление памяти объектов с переопределенными методами equals() и hashCode()

Другим примером является использование ORM, например Hibernate, который использует методы equals() и hashCode() для анализа объектов и сохранения их в кеше. Если эти методы не переопределены, то шансы утечки памяти довольно высоки, потому что Hibernate не сможет сравнивать объекты и заполнит свой кеш их дубликатами.

Третий сценарий связан со строковыми операциями. Вызов метода substring() у строки, возвращается экземпляр String с лишь изменёнными значениями переменных length и offset — длины и смещения char-последовательности. При этом, если получить строку длиной 5000 символов и получить её префикс, используя метод substring(), то 5000 символов будут продолжать храниться в памяти. Для систем, которые получают и обрабатывают множество сообщений, это может быть серьёзной проблемой.

Избежать данной проблемы можно, используя любой из предложенных вариантов:

String prefix = new String(longString.substring(0,5)); // Первыйвариант

String prefix = longString.substring(0,5).intern(); // Второйвариант

Заключение

Борьба с утечками памяти вообще довольно таки нетривиальная и сложная задача, и данная проблема может встретиться не только в высоконагруженных или корпоративных веб-приложениях. Часто для решения таких проблем приходится углубляться в чужой исходный код. В общем случае можно рекомендовать каждый раз при использовании той или иной технологии (будь это фреймворк или сторонняя библиотека) проверять, вызывает ли она утечки памяти или нет. С другой стороны, можно не обращать внимание на утечки памяти. С подобными утечками система может стабильно работать годами, при этом просто потребляя при этом памяти больше, чем нужно. Однако, это является серьезной ошибкой, и неизвестно в какой момент времени она может остановить работу предприятия.

Литература:

  1. Container first. — Текст: электронный // Quarkus — Susersonic Subatomic Java: [сайт]. — URL: https://quarkus.io/ (дата обращения: 12.05.2020).
  2. TCMalloc: Thread-Caching Malloc. — Текст: электронный // TCMalloc: [сайт]. — URL: http://goog-perftools.sourceforge.net/doc/tcmalloc.html (дата обращения: 16.05.2020).
  3. Сборщик мусора G1 в Java 9. — Текст: электронный // urvanov.ru: [сайт]. — URL: https://urvanov.ru/2018/03/25/ %D1 %81 %D0 %B1 %D0 %BE %D1 %80 %D1 %89 %D0 %B8 %D0 %BA- %D0 %BC %D1 %83 %D1 %81 %D0 %BE %D1 %80 %D0 %B0-g1- %D0 %B2-java-9/ (дата обращения: 17.05.2020).
  4. GraalVM: Clearing up confusion around the term and why Twitter uses it in production. — Текст: электронный // JAXenter: [сайт]. — URL: https://jaxenter.com/graalvm-chris-thalinger-interview-163074.html (дата обращения: 17.05.2020).
  5. Top 10 Things To Do With GraalVM. — Текст: электронный // Medium: [сайт]. — URL: https://medium.com/graalvm/graalvm-ten-things-12d9111f307d (дата обращения: 24.05.2020).
  6. Static Compilation of Java Applications at Alibaba at Scale. — Текст: электронный // Medium: [сайт]. — URL: https://medium.com/graalvm/static-compilation-of-java-applications-at-alibaba-at-scale-2944163c92e.
  7. Из 8 в 13: полный обзор версий Java. Часть 2. — Текст: электронный // JavaRush: [сайт]. — URL: https://javarush.ru/groups/posts/2549-iz-8-v-13-polnihy-obzor-versiy-java-chastjh-2 (дата обращения: 26.05.2020).
  8. GraalVM. — Текст: электронный // Википедия: [сайт]. — URL: https://ru.wikipedia.org/wiki/GraalVM (дата обращения: 27.05.2020).
  9. Understanding Memory Leaks in Java. — Текст: электронный // Baeldung: [сайт]. — URL: https://www.baeldung.com/java-memory-leaks (дата обращения: 27.05.2020).
  10. GraalVM Native Image Compatibility and Optimization Guide. — Текст: электронный // GitHub: [сайт]. — URL: https://github.com/oracle/graal/blob/master/substratevm/LIMITATIONS.md (дата обращения: 29.05.2020).
Основные термины (генерируются автоматически): JVM, JDK, JIT, CPU, потребление памяти, утечка памяти, REST, память, сборщик мусора, ZGC.


Ключевые слова

память, Java, JVM, сборщик мусора, CPU, выделение памяти, утечка памяти

Похожие статьи

Типовые атаки на DHCP

В статье авторы рассматривают типовые атаки на DHCP и рассказывают о методах обеспечения сетевой безопасности, необходимых для эффективной защиты от угроз. В данной статье меры безопасности моделируются с использованием Cisco Packet Tracer.

Обработка конкурентных транзакций в распределенных системах на примере Java

При разработке программного обеспечения в высоконагруженных системах требуется определить стратегии при одновременных обновлениях. Множество запросов от одинаковых пользователей приводят к конфликтам транзакций на уровне базы данных. Для предотвращен...

Диагностика утечек памяти в Java-приложениях

Данная статья описывает структуру хранения используемой памяти, простые способы диагностики её утечек и временные исправления их.

Применение простой стеганографии при передаче файлов в интернете

В данной статье описывается понятие стеганографии, а также показывается её применение на простейших примерах в операционной системе Windows.

Защита корпоративных сетей от внутренних атак

В данной статье исследуется разработка корпоративной сети на базе технологии Multiprotocol Label Switching (MPLS) и стратегии защиты от атак типа Address Resolution Protocol (ARP) spoofing и троянских программ [1]. Основой стратегии безопасности служ...

Исследование процессов внутри виртуальной машины Java

В статье подробно описываются процессы виртуальной машины Javа, на что выделяется память, как устроена JVM, как в нее попадает код и как он исполняется.

Исследование изменения скорости выполнения программ из-за промахов кэша процессора

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

Разработка и внедрение библиотеки валидации на клиентском языке JavaScript

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

Работа с баг-трекером: эффективное управление ошибками в разработке программного обеспечения

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

Оценка рисков в ИТ-проектах на ранних этапах

Существует множество исследований, советов и практических рекомендаций, как управлять рисками в ИТ-проектах (информационные технологии). Менеджеру проекта кажется, что он знает, что нужно делать для управления рисками, но не всегда это приводит к усп...

Похожие статьи

Типовые атаки на DHCP

В статье авторы рассматривают типовые атаки на DHCP и рассказывают о методах обеспечения сетевой безопасности, необходимых для эффективной защиты от угроз. В данной статье меры безопасности моделируются с использованием Cisco Packet Tracer.

Обработка конкурентных транзакций в распределенных системах на примере Java

При разработке программного обеспечения в высоконагруженных системах требуется определить стратегии при одновременных обновлениях. Множество запросов от одинаковых пользователей приводят к конфликтам транзакций на уровне базы данных. Для предотвращен...

Диагностика утечек памяти в Java-приложениях

Данная статья описывает структуру хранения используемой памяти, простые способы диагностики её утечек и временные исправления их.

Применение простой стеганографии при передаче файлов в интернете

В данной статье описывается понятие стеганографии, а также показывается её применение на простейших примерах в операционной системе Windows.

Защита корпоративных сетей от внутренних атак

В данной статье исследуется разработка корпоративной сети на базе технологии Multiprotocol Label Switching (MPLS) и стратегии защиты от атак типа Address Resolution Protocol (ARP) spoofing и троянских программ [1]. Основой стратегии безопасности служ...

Исследование процессов внутри виртуальной машины Java

В статье подробно описываются процессы виртуальной машины Javа, на что выделяется память, как устроена JVM, как в нее попадает код и как он исполняется.

Исследование изменения скорости выполнения программ из-за промахов кэша процессора

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

Разработка и внедрение библиотеки валидации на клиентском языке JavaScript

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

Работа с баг-трекером: эффективное управление ошибками в разработке программного обеспечения

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

Оценка рисков в ИТ-проектах на ранних этапах

Существует множество исследований, советов и практических рекомендаций, как управлять рисками в ИТ-проектах (информационные технологии). Менеджеру проекта кажется, что он знает, что нужно делать для управления рисками, но не всегда это приводит к усп...

Задать вопрос