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

Создаем объекты лишь тогда, когда нужно
Пожалуй, создание объектов является одной из наиболее затратных операций в Java. Хорошая практика — создавать их лишь тогда, когда они действительно необходимы.

Deadlocks
Взаимная блокировка потоков (дэдлок) бывает по множеству причин, а полностью уберечься от них в Java 8 весьма сложно. Зачастую, такое происходит, если один синхронизируемый объект ожидает ресурсов, а они заблокированы другим синхронизированным объектом.
Рассмотрим пример тупика потока:

Результат программы будет следующим:

Если изменить порядок вызова потоков, то взаимоблокировок можно избежать:


Резервирование памяти
Ряд Java-приложений обладает повышенной ресурсоемкостью, что может приводить к замедленной работе. Чтобы повысить производительность, можно выделить на Java-машине больше оперативной памяти.

Здесь: • Xms — минимальный пул выделения памяти; • Xmx — максимальный пул; • XX: PermSize — начальный размер, который выделится при запуске JVM; • XX: MaxPermSize — максимальный размер, который выделится при запуске JVM.
Может ли Java-приложение использовать больше памяти, чем размер кучи

Возможно, вы замечали, что Java-приложение может превышать лимит памяти, указанный в параметре -Xmx (максимальный размер кучи). Дело в том, что JVM помимо кучи использует и другие области памяти.
Давайте начнем со структуры памяти Java-приложения и источников потребления памяти.
2. Структура памяти Java-процесса
Память JVM состоит из двух основных частей: память кучи (heap) и память вне кучи (non-heap).
Куча (heap) — весьма известная область памяти. JVM инициализирует кучу при старте и размещает в ней объекты, создаваемые приложением. Когда объект становится не нужен, сборщик мусора (garbage collector, GC) освобождает занимаемую им память. Таким образом, при работе приложения размер кучи изменяется. Ее максимальный размер устанавливается с помощью параметра -Xmx.
Вся остальная используемая память — это память за пределами кучи. В ней и кроется причина использования приложением большего объема памяти, чем указывается в -Xmx. Память вне кучи разделена на несколько областей: код JVM, внутренние структуры, код агента профилировщика, структуры классов, такие как пул констант, метаданные полей и методов, а также код методов, конструкторов и интернированные строки.
Стоит отметить, что некоторые области памяти вне кучи настраиваются через параметры -XX, например, -XX:MaxMetaspaceSize (эквивалентно -XX:MaxPermSize в Java 7 и более ранних версиях). Об этом параметре и других мы поговорим далее.
Память вне кучи может потребляться не только JVM — мы сами можем выделить вне кучи прямые буферы ByteBuffers, которые не будут находиться под контролем сборщика мусора. Также память вне кучи могут использовать нативные библиотеки.
3. Области памяти JVM вне кучи
3.1. Metaspace
Metaspace — это нативная область памяти, в которой хранятся метаданные классов. Когда загрузчик классов (class loader) и все загруженные им классы удаляются из кучи, тогда сборщик мусора может удалить их данные из Metaspace.
Однако освобожденное в Metaspace пространство не обязательно возвращается в операционную систему. JVM может удерживать всю или часть этой памяти для повторного использования.
До Java 8 область Metaspace называлась Permanent Generation (PermGen), но в отличие от Metaspace, PermGen располагалась в куче.
3.2. Кэш кода (Code Cache)
Just-In-Time (JIT) компилятор сохраняет результаты своей работы в области кэша кода. Часто выполняемые участки («горячие точки», Hotspots) компилируются JIT-компилятором в машинный код. При многоуровневой компиляции, появившийся в Java 7, используется два компилятора: сначала клиентский компилятор (C1) компилирует код с профилированием, а затем серверный компилятор (C2) использует данные профилирования для оптимизированной компиляции.
Цель многоуровневой компиляции — совместное использование компиляторов C1 и C2 для обеспечения как быстрого запуска программы, так и хорошей производительности при длительной работе. Многоуровневая компиляция увеличивает объем кода, который необходимо кэшировать в памяти, до четырех раз. Начиная с Java 8, многоуровневая компиляция включена по умолчанию, но ее можно отключить.
3.3. Потоки (Thread)
Стек потока содержит локальные переменные каждого выполняемого метода и методов, вызванных в этом потоке для достижения текущей точки выполнения. Стек потока доступен только потоку, который его создал.
Теоретически, поскольку память стека потоков зависит от количества потоков, а количество потоков может быть любым, размер этой области не ограничен. В реальности операционная система ограничивает количество потоков, а в JVM есть значение по умолчанию для размера памяти стека потока, которое зависит от платформы.
3.4. Сборщик мусора (Garbage Collection)
JVM поставляется с разными сборщиками мусора, и, в зависимости от типа нагрузки, можно выбрать наиболее подходящий вариант. Любой сборщик мусора использует нативную память, размер которой зависит от конкретного сборщика мусора.
3.5. Символы (Symbol)
JVM использует область Symbol для хранения символов, таких как имена полей, сигнатуры методов и интернированные строки. В JDK символы хранятся в трех разных таблицах:
- System Dictionary (системный словарь) содержит информацию о загруженных типах, таких как классы.
- Constant Pool (пул констант) использует Symbol Table (таблицу символов) для хранения загруженных символов классов, методов, полей и перечислений. JVM поддерживает для каждого типа Run-Time Constant Pool, содержащий несколько видов констант начиная от числовых литералов времени компиляции до методов в рантайме и ссылок на поля.
- String Table (таблица строк) содержит ссылки на все константные строки, которые также называются интернированными строками.
Чтобы разобраться с таблицей строк (String Table) сначала поговорим о пуле строк (String Pool). Пул строк — это оптимизация использования памяти объектами String, когда сохраняется только одна копия каждой литеральной строки в пуле с помощью механизма, называемого интернированием. Пул строк состоит из двух частей:
- Содержимое интернированных строк, хранящихся в куче как обычные объекты String.
- Хеш-таблица, также называемая «таблицей строк» (String Table), размещенная вне кучи и содержащая ссылки на интернированные строки.
Другими словами, пул строк использует память как в куче, так и вне кучи. Память вне кучи — это String Table. Таблица строк обычно небольшая, но все же может занять значительный объем, когда у вас много интернированных строк.
3.6. Arena
Arena — это реализация в JVM управления памятью на основе регионов (Arena), которая отличается от реализации в glibc. Она используется некоторыми подсистемами JVM, такими как компилятор и символы, а также при использовании нативным кодом внутренних объектов, полагающихся на арены JVM.
3.7. Прочее
Все другие виды использования памяти, не попадающие в указанные выше категории, относятся к этому разделу. Например, использование DirectByteBuffer.
4. Инструменты мониторинга памяти
Теперь, когда мы разобрались с тем, что использование памяти в Java не ограничивается кучей, посмотрим на инструменты анализа памяти и параметры JVM.
Из коробки в JDK есть следующие инструменты для мониторинга памяти:
- jmap — утилита командной строки для анализа распределения памяти (memory map) java-процесса. jmap также позволяет подключаться к процессу на удаленной машине. Однако после появления jcmd в JDK8 рекомендуется использовать именно jcmd вместо jmap из-за лучшей диагностики и меньших накладных расходов.
- jcmd используется для отправки диагностических команд в JVM, в том числе для управления Java Flight Recorder, траблшутинга, диагностики JVM и приложений. Но jcmd не работает с удаленными процессами. Далее мы посмотрим на некоторые примеры использования jcmd.
- jhat визуализирует файл дампа кучи. Создать дамп можно несколькими способами, например, с помощью jmap -dump или jcmd GC.heap_dump filename.
- hprof (Heap/CPU Profiling Tool) предоставляет данные об использовании процессора, статистике выделения памяти в куче и др. В зависимости от типа профилирования, hprof инструктирует виртуальную машину собирать соответствующие события JVM Tool Interface (JVM TI) и преобразует собранные данные в форму, доступную для дальнейшего анализа.
Помимо инструментов, поставляемых с JVM, в операционных системах есть свои инструменты для анализа памяти. Например, pmap в Linux предоставляет полную информацию о памяти процесса.
5. Native Memory Tracking
Native Memory Tracking (NMT) позволяет анализировать внутреннее использование памяти JVM. Несмотря на то что NMT не способен отследить абсолютно всю используемую память, например, он не увидит выделение памяти в нативном стороннем коде, его функциональности достаточно для большинства типичных приложений.
NMT необходимо включить при запуске приложения:
java -XX:NativeMemoryTracking=summary -jar app.jar
Для параметра -XX:NativeMemoryTracking доступны также значения off и detail. Имейте в виду, что включение NMT связано с накладными расходами, влияющими на производительность. Также увеличивается потребляемая память: добавляется по два машинных слова на каждый malloc.
Найти идентификатор процесса (pid) java-приложения можно с помощью jps или jcmd без аргументов:
jcmd
Доступные команды jcmd посмотрим с помощью help:
jcmd help
Из вывода мы видим, что в jcmd доступны различные категории команд, такие как Compiler, GC, JFR, JVMTI, ManagementAgent и VM. Некоторые команды, такие как VM.metaspace, VM.native_memory, могут помочь нам с анализом памяти.
5.1. Сводка об использовании внутренней памяти
Для получения сводной информации об используемой внутренней нативной памяти предназначена команда VM.native_memory:
jcmd VM.native_memory summary : Native Memory Tracking: Total: reserved=1779287KB, committed=503683KB - Java Heap (reserved=307200KB, committed=307200KB) . - Class (reserved=1089000KB, committed=44824KB) . - Thread (reserved=41139KB, committed=41139KB) . - Code (reserved=248600KB, committed=17172KB) . - GC (reserved=62198KB, committed=62198KB) . - Compiler (reserved=175KB, committed=175KB) . - Internal (reserved=691KB, committed=691KB) . - Other (reserved=16KB, committed=16KB) . - Symbol (reserved=9704KB, committed=9704KB) . - Native Memory Tracking (reserved=4812KB, committed=4812KB) . - Shared class space (reserved=11136KB, committed=11136KB) . - Arena Chunk (reserved=176KB, committed=176KB) . - Logging (reserved=4KB, committed=4KB) . - Arguments (reserved=18KB, committed=18KB) . - Module (reserved=175KB, committed=175KB) . - Safepoint (reserved=8KB, committed=8KB) . - Synchronization (reserved=4235KB, committed=4235KB) .
В выводе jcmd мы видим сводку по областям памяти JVM, таким как Java Heap (куча), GC (сборщик мусора), Thread (поток) и т.д. Зарезервированная память (reserved) — это общий диапазон адресов, предварительно отображенный (pre-mapped) с помощью malloc или mmap, — это максимальная адресуемая память для данной области. Выделенная память (commited) — это активно используемая память.
Более подробное объяснение вывода вы можете найти здесь. Чтобы увидеть изменения в использовании памяти, можно последовательно использовать VM.native_memory baseline и VM.native_memory summary.diff.
5.2. Metaspace и String Table
Другие области памяти, например, Metaspace, символы и интернированные строки, также доступны для анализа.
Давайте взглянем на Metaspace:
jcmd VM.metaspace
Результат выглядит следующим образом:
: Total Usage - 1072 loaders, 9474 classes (1176 shared): . Virtual space: Non-class space: 38.00 MB reserved, 36.67 MB ( 97%) committed Class space: 1.00 GB reserved, 5.62 MB ( <1%) committed Both: 1.04 GB reserved, 42.30 MB ( 4%) committed Chunk freelists: Non-Class: . Class: . Waste (percentages refer to total committed size 42.30 MB): Committed unused: 192.00 KB ( <1%) Waste in chunks in use: 2.98 KB ( <1%) Free in chunks in use: 1.05 MB ( 2%) Overhead in chunks in use: 232.12 KB ( <1%) In free chunks: 77.00 KB ( <1%) Deallocated from chunks in use: 191.62 KB ( <1%) (890 blocks) -total-: 1.73 MB ( 4%) MaxMetaspaceSize: unlimited CompressedClassSpaceSize: 1.00 GB InitialBootClassLoaderMetaspaceSize: 4.00 MB
Далее таблица строк (String Table):
jcmd VM.stringtable
: StringTable statistics: Number of buckets : 65536 = 524288 bytes, each 8 Number of entries : 20046 = 320736 bytes, each 16 Number of literals : 20046 = 1507448 bytes, avg 75.000 Total footprint : = 2352472 bytes Average bucket size : 0.306 Variance of bucket size : 0.307 Std. dev. of bucket size: 0.554 Maximum bucket size : 4
6. Настройка памяти JVM
Итак, общее потребление памяти складывается из размера кучи и памяти вне кучи, выделяемой компонентами JVM или сторонними библиотеками.
В процессе работы приложения память вне кучи изменяется незначительно. Обычно ее размер стабилизируется после загрузки всех используемых классов и полного прогрева JIT. Для настройки областей памяти JVM есть различные параметры JVM.
Параметры, с которыми был запущен Java-процесс (включая значения по умолчанию) можно посмотреть с помощью VM.flags:
jcmd VM.flags
В результате выводятся параметры и их значения:
: -XX:CICompilerCount=4 -XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=8 -XX:G1HeapRegionSize=1048576 -XX:InitialHeapSize=314572800 .
Далее рассмотрим некоторые параметры виртуальной машины для настройки памяти.
6.1. Куча (Heap)
Для настройки кучи есть множество параметров JVM. Первоначальный и максимальный размер задаются параметрами -Xms (-XX:InitialHeapSize) и -Xmx (-XX:MaxHeapSize). Для задания размера кучи в процентах от физической памяти используются параметры -XX:MinRAMPercentage и -XX:MaxRAMPercentage. Имейте в виду, что JVM игнорирует эти два параметра, если заданы -Xms и -Xmx.
Еще один параметр, влияющий на распределение памяти, — XX:+AlwaysPreTouch. По умолчанию куча с заданным максимальным размером выделяется в виртуальной памяти, а не в физической. Операционная система может решить не выделять память до тех пор, пока не будет операций записи. Для обхода этого (особенно при использовании огромных DirectByteBuffers, когда перераспределение страниц памяти может занять существенное время), можно включить -XX:+AlwaysPreTouch. Pretouching записывает "0" на все страницы и заставляет операционную систему выделять память, а не просто резервировать. Pretouching также приводит к более долгому запуску JVM, так как работает в одном потоке.
6.2. Стек потока (Thread stack)
Стек потока — это индивидуальное, на каждый поток, хранилище локальных переменных метода. Для настройки размера используются параметры -Xss или XX:ThreadStackSize. Размер стека потока по умолчанию зависит от платформы, но в большинстве современных 64-битных операционных систем составляет до 1 МБ.
6.3. Сборщик мусора (Garbage Collector)
Для выбора сборщика мусора предназначены параметры: -XX:+UseSerialGC, -XX:+UseParallelGC, -XX:+UseParallelOldGC, -XX:+UseConcMarkSweepGC или -XX:+UseG1GC.
Для сборщика мусора G1 можно дополнительно включить дедупликацию строк с помощью -XX:+UseStringDeduplication, что может сэкономить значительный процент памяти. Дедупликация строк применяется только к долгоживущим экземплярам. Порог возраста экземпляров в виде количества пережитых циклов сборки мусора указывается в параметре -XX:StringDeduplicationAgeThreshold.
6.4. Кэш кода (Code Cache)
Начиная с Java 9, кэш кода состоит из трех сегментов, размер которых настраивается следующими опциями JVM:
- -XX:NonNMethodCodeHeapSize — размер non-method сегмента, содержащего внутренний код JVM. По умолчанию около 5 МБ.
- -XX:ProfiledCodeHeapSize — размер profiled сегмента с потенциально коротким временем жизни, который представляет собой скомпилированный код C1. По умолчанию около 122 МБ.
- -XX:NonProfiledCodeHeapSize — размер non-profiled сегмента с потенциально длительным временем жизни, который представляет собой скомпилированный код C2. По умолчанию около 122 МБ.
6.5. Аллокаторы памяти
JVM при старте резервирует память, которая затем становится доступной через изменение отображения памяти, используя malloc и mmap из glibc. Резервирование и освобождение фрагментов памяти может приводить к фрагментации. Следствие фрагментации — образование большого количества неиспользуемых областей памяти.
Помимо malloc, доступны другие аллокаторы, например, jemalloc или tcmalloc. В аллокаторе jemalloc особое внимание уделяется предотвращению фрагментации и поддержке масштабируемой конкурентности, поэтому он часто эффективнее, чем стандартный malloc из glibc. Кроме того, jemalloc также можно использовать для анализа утечек памяти и профилирования кучи.
6.6. Metaspace
Для настройки нижней и верхней границы размера Metaspace используются параметры -XX:MetaspaceSize и -XX:MaxMetaspaceSize соответственно.
Также полезен параметр -XX:InitialBootClassLoaderMetaspaceSize для настройки первоначального размера области памяти загрузчика классов.
Параметры -XX:MinMetaspaceFreeRatio и -XX:MaxMetaspaceFreeRatio задают минимальный и максимальный процент свободной памяти для метаданных классов после сборки мусора.
Мы также можем настроить максимальный размер расширения Metaspace без полной сборки мусора с помощью -XX:MaxMetaspaceExpansion.
6.7. Другие области памяти вне кучи
Размер пула строк задается с помощью параметра -XX:StringTableSize. В этом параметре указывается максимальное количество различных интернированных строк. Для JDK7+ значение по умолчанию 60013.
Для контроля использования DirectByteBuffers предназначен параметр -XX:MaxDirectMemorySize. С помощью него мы ограничиваем объем памяти, который может быть зарезервирован для всех DirectByteBuffers.
Для приложений, которым необходимо загрузить большое количество классов, можно использовать опцию -XX:PredictedLoadedClassCount. Этот параметр доступен с JDK8 и позволяет установить размер бакета системного словаря (System Dictionary).
7. Заключение
В этой статье мы рассмотрели области памяти Java-процесса и несколько инструментов для мониторинга использования памяти. С помощью jcmd мы увидели, что память Java-приложения не ограничивается кучей, и поговорили о некоторых параметрах JVM для настройки использования памяти.
Перевод статьи подготовлен в преддверии старта курса "Java Developer. Professional". Приглашаю всех на бесплатный урок курса, где поговорим о том, как кэширование помогает улучшить производительность Java-приложений. Рассмотрим самые простые реализации на базе HashMap и популярные решения Ehcache и Caffeine.
java -Xms -Xmx за что отвинчивает этот параметр? я понял что за ОЗУ под ВМ ну подробней можно)
фанат, не надо свистеть что только мегабайты.
-Xmx1024k
-Xmx512m
-Xmx8g
всё это допустимые значения.
Остальные ответы
-Xms - минимальное ОЗУ
-Xmx - максимальное ОЗУ
В общем выставление разрешимого кол-ва ОЗУ для использование
от 1024 до 2048 например
Del eteМастер (1204) 8 лет назад
спасибо) а то всегда тыкаю одинаковые числа что туда что туда) а у Gb указывать можно?
waac Гуру (3987) нет, только мегабайт
Похожие вопросы
Ваш браузер устарел
Мы постоянно добавляем новый функционал в основной интерфейс проекта. К сожалению, старые браузеры не в состоянии качественно работать с современными программными продуктами. Для корректной работы используйте последние версии браузеров Chrome, Mozilla Firefox, Opera, Microsoft Edge или установите браузер Atom.
Параметры -Xms и -Xmx при запуске JVM
Часто при работе с Java разработчики сталкиваются с такими параметрами запуска JVM, как -Xms и -Xmx . Они отвечают за управление памятью в виртуальной машине Java и могут существенно повлиять на производительность и стабильность работы приложения.
Что такое -Xms и -Xmx ?
Параметр -Xms задает начальный размер (в байтах) кучи, которую JVM выделяет при старте приложения. Это важно, поскольку недостаток памяти при старте может привести к тому, что приложение будет работать медленно или вообще не сможет запуститься.
Параметр -Xmx , наоборот, задает максимальный размер (также в байтах) кучи. Если приложение пытается использовать больше памяти, чем задано параметром -Xmx , то JVM может выбросить исключение OutOfMemoryError .
Пример использования
java -Xms128m -Xmx1024m MyApplication
В этом примере при запуске приложения MyApplication виртуальная машина Java выделит начально 128 мегабайт памяти, а максимальный размер кучи составит 1024 мегабайт (или 1 гигабайт).
Значения по умолчанию
Значения параметров -Xms и -Xmx по умолчанию зависят от конкретной реализации JVM и могут варьироваться. Обычно начальный размер кучи составляет несколько десятков мегабайт, а максимальный — от половины до четверти от всего доступного объема оперативной памяти.
Заключение
Правильное использование параметров -Xms и -Xmx может существенно улучшить производительность и стабильность работы Java-приложений. Однако следует помнить, что задание слишком большого объема памяти может привести к неэффективному использованию ресурсов и замедлению работы всей системы.