Отслеживание локальной памяти в JVM

JVM

1 Обзор

Вы когда-нибудь думали о том, почему приложения Java потребляют больше, чем память, чем из известных -xms и -xmx Tuning? По различным причинам и возможной оптимизации JVM может выделить дополнительную родную память. Эти дополнительные распределения в конечном итоге сделают потребляемую память за пределы ограничения -xmx.

В этом руководстве мы перечислим некоторые распространенные источники выделения памяти в JVM вместе с их флагами изменения размера, а затем узнаем, как отслеживать их с помощью встроенного отслеживания памяти.

2. Собственное распределение

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

2.1. Метапространство

Для хранения некоторых метаданных о загруженных классах JVM использует выделенную область вне кучи, называемую метапространством. До Java 8 он назывался PermGen или постоянное поколение. Metaspace или PermGen содержат метаданные о загруженных классах, а не об их экземплярах, которые хранятся в куче.

Здесь важно то, что конфигурация размера кучи не влияет на размер метапространства, поскольку метапространство — это область данных вне кучи. Чтобы ограничить размер метапространства, мы используем другие флаги настройки:

  • -XX:MetaspaceSize и -XX:MaxMetaspaceSize устанавливают минимальный и максимальный размер метапространства.
  • До Java 8 параметры -XX:PermSize и -XX:MaxPermSize устанавливали минимальный и максимальный размер PermGen.

2.2. Темы

Одной из самых ресурсоемких областей данных в JVM является стек, который создается одновременно с каждым потоком. Стек хранит локальные переменные и частичные результаты и играет важную роль в вызовах методов.

Размер стека потоков по умолчанию зависит от платформы, но в большинстве современных 64-разрядных операционных систем он составляет около 1 МБ. Этот размер настраивается с помощью флага настройки -Xss.

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

2.3 Кэш кода

Чтобы запустить байт-код JVM на разных платформах, его необходимо преобразовать в машинные инструкции. Компилятор JIT отвечает за эту компиляцию при выполнении программы.

Когда JVM компилирует байт-код в ассемблерные инструкции, она сохраняет эти инструкции в специальной области данных вне кучи, называемой кешем кода. Кэш кода может управляться так же, как и любой другой областью данных в JVM.-XX:InitialCodeCacheSizeа также-XX:ReservedCodeCacheSizeФлаги настройки определяют начальное и возможное максимальное значение кэша кода.

2.4 Сборка мусора

JVM поставляется с несколькими алгоритмами GC, каждый из которых подходит для разных вариантов использования. У всех этих алгоритмов GC есть одна общая черта: им необходимо использовать некоторые структуры данных вне кучи для выполнения своих задач. Эти внутренние структуры данных потребляют больше собственной памяти.

2.5 Символы

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

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

JVM хранит фактически сохраненные строки в собственной специальной хеш-таблице фиксированного размера, называемой таблицей строк, также известной какСтрунный пул. мы можем пройти-XX:StringTableSizeНастройте размер таблицы конфигурации флагов (т. е. количество сегментов).

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

2.6. Собственные байтовые буферы

JVM часто подозревают в выделении большого количества собственной памяти, но иногда разработчики могут выделять собственную память напрямую. Наиболее распространенными методами являются malloc, вызываемые JNI, и ByteBuffers, вызываемые непосредственно в NIO.

2.7. Дополнительные флаги настройки

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

$ java -XX:+PrintFlagsFinal -version | grep <concept>

PrintFlagsFinal печатает все параметры -XX в JVM. Например, чтобы найти все флаги, связанные с Metaspace:

$ java -XX:+PrintFlagsFinal -version | grep Metaspace
      // truncated
      uintx MaxMetaspaceSize                          = 18446744073709547520                    {product}
      uintx MetaspaceSize                             = 21807104                                {pd product}
      // truncated

3. Отслеживание встроенной памяти (NMT)

Теперь, когда мы увидели распространенные источники выделения собственной памяти в JVM, пришло время выяснить, как их отслеживать. Во-первых, мы должны включить собственное отслеживание памяти с другим флагом настройки JVM:-XX:NativeMemoryTracking = off | sumary | detail. По умолчанию NMT выключен, но мы можем сделать так, чтобы он видел сводку или подробный обзор своих наблюдений.

Предположим, мы хотим отслеживать собственные распределения для типичного приложения Spring Boot:

$ java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseG1GC -jar app.jar

Здесь мы включаем NMT, выделяя 300 МБ пространства в куче, используя G1 в качестве нашего алгоритма GC.

3.1. Снимки экземпляра

С включенным NMT мы можем получить информацию о собственной памяти в любое время с помощью команды jcmd:

$ jcmd <pid> VM.native_memory

Чтобы найти PID приложения JVM, мы можем использовать команду jps:

$ jps -l                    
7858 app.jar // This is our app
7899 sun.tools.jps.Jps

Теперь, если мы используем jcmd с соответствующим pid, VM.native_memory заставляет JVM распечатывать информацию о собственных выделениях:

$ jcmd 7858 VM.native_memory

Давайте проанализируем вывод NMT по частям.

3.2 Общее распределение

NMT сообщает обо всей зарезервированной и выделенной памяти следующим образом:

Native Memory Tracking:
Total: reserved=1731124KB, committed=448152KB

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

Несмотря на выделение 300 МБ кучи, общий объем зарезервированной памяти нашего приложения составляет почти 1,7 ГБ, что намного больше. Точно так же выделенная память составляет около 440 МБ, что снова значительно превышает 300 МБ.

После обзора NMT сообщает о выделении памяти для каждого источника выделения. Итак, давайте углубимся в каждый источник.

3.3. Куча

NMT сообщает о выделении кучи, как мы и ожидали:

Java Heap (reserved=307200KB, committed=307200KB)
          (mmap: reserved=307200KB, committed=307200KB)

300 МБ зарезервированной и выделенной памяти, что соответствует нашей настройке размера кучи.

3.4. Метапространство

Вот отчет NMT по метаданным загруженных классов:

Class (reserved=1091407KB, committed=45815KB)
      (classes #6566)
      (malloc=10063KB #8519) 
      (mmap: reserved=1081344KB, committed=35752KB)

Зарезервировано почти 1 Гб, 45 Мб отведено под загрузку 6566 классов.

3.5. Тема

Вот отчет NMT о распределении потоков:

Thread (reserved=37018KB, committed=37018KB)
       (thread #37)
       (stack: reserved=36864KB, committed=36864KB)
       (malloc=112KB #190) 
       (arena=42KB #72)

Всего на стеки из 37 потоков выделяется 36 МБ памяти — примерно по 1 МБ каждый. JVM выделяет память потокам во время создания, поэтому зарезервированные и зафиксированные выделения равны.

3.6 Кэш кода (буфер кода)

Давайте взглянем на отчет NMT о сгенерированных JIT и кэшированных инструкциях по сборке:

Code (reserved=251549KB, committed=14169KB)
     (malloc=1949KB #3424) 
     (mmap: reserved=249600KB, committed=12220KB)

В настоящее время кэшируется около 13 МБ кода, этот объем может достигать 245 МБ.

3.7. GC

Вот отчет NMT об использовании памяти G1 GC:

GC (reserved=61771KB, committed=61771KB)
   (malloc=17603KB #4501) 
   (mmap: reserved=44168KB, committed=44168KB)

Мы видим, что и зарезервировано, и выделено около 60 МБ, предназначенных для помощи G1.

Давайте посмотрим на использование памяти более простыми сборщиками мусора, такими как последовательный сборщик мусора:

$ java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseSerialGC -jar app.jar

Serial GC использует почти менее 1 МБ:

GC (reserved=1034KB, committed=1034KB)
   (malloc=26KB #158) 
   (mmap: reserved=1008KB, committed=1008KB)

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

3.8 Символ

Вот отчет NMT о распределении символов, таких как таблицы строк и пулы констант:

Symbol (reserved=10148KB, committed=10148KB)
       (malloc=7295KB #66194) 
       (arena=2853KB #1)

Под символы отведено почти 10 МБ.

3.9 NMT с течением времени

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

$ jcmd <pid> VM.native_memory baseline
Baseline succeeded

Затем, через некоторое время, мы можем сравнить текущее использование памяти с этим базовым уровнем:

$ jcmd <pid> VM.native_memory summary.diff

Использование NMT символов + и - расскажет нам, как изменилось использование памяти за этот период:

Total: reserved=1771487KB +3373KB, committed=491491KB +6873KB
-             Java Heap (reserved=307200KB, committed=307200KB)
                        (mmap: reserved=307200KB, committed=307200KB)

-             Class (reserved=1084300KB +2103KB, committed=39356KB +2871KB)
// Truncated

Общий объем зарезервированной и выделенной памяти увеличился на 3 МБ и 6 МБ соответственно. Другие колебания в распределении памяти можно легко обнаружить.

3.10 Подробная информация о НМТ

NMT может предоставить очень подробную информацию обо всей карте дискового пространства. Чтобы включить этот подробный отчет, мы должны использовать-XX:NativeMemoryTracking =detailЗнак корректировки информации.

4. Вывод

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

оригинал:Эй, поставь Al Lington.com/Native-Memo...

автор:Ali Dehghani

Переводчик: Эмма