Найти в Дзене
(java || kotlin) && devOps

Память в JVM - показать то, что скрыто

Сегодня поговорим про память в JVM.

Все знают про heap и stack. Начнем с них.

1) heap, она же куча. Хранит все создаваемые при работе JVM объекты. Очищается сборщиком мусора - garbage collector.

Делится на поколения: Eden, Survivor и Tenured (Old), между которыми перемещаются выжившие при сборке мусора объекты.

Кроме объектов начиная с Java 7 хранит также пул строк и другие пулы. Другие пулы - это пулы базовых типов-обверток над примитивами с фиксированным числом значений. Т.е. Boolean, Byte, Short, Integer, Long, Character. Цель использования пулов - уменьшение объема используемой памяти.

Раз пулы находятся в куче, то можно предположить, что неиспользуемые значения также удаляются сборщиком мусора. Так и есть.

Размер кучи можно регулировать параметрами: java -Xms256m -Xmx2048m

-Xms - начальный размер

-Xmx - максимальный размер.

По умолчанию максимальный размер для 64 разрядной системы при работе в сервером режиме равен 1/4 оперативной памяти, но не более 32 Гб.

https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/parallel.html#default_heap_size

Посмотреть размер памяти у запущенного процесса:

java -XX:+PrintFlagsFinal <GC options> -version | grep MaxHeapSize.

Размер пулов тоже настраивается, пример для пула строк:

-XX:StringTableSize=4901

Если памяти не хватило - вылетает OutOfMemoryError: Java heap space

2) stack. Хранит стек вызовов всех Java потоков. В стеке хранятся локальные переменные и параметры примитивных типов, а также ссылки на объекты локальных переменных и параметров. Сами объекты при этом хранятся в куче.

Размер регулируется параметром -Xss. Как правило занимает немного, хотя в теории 100 потоков - это 100 Мб. Не стоит забывать, что кроме созданных в коде потоков есть служебные - потоки сборщика мусора и потоки JIT компилятора. Если памяти не хватило, а это как правило означает бесконечную рекурсию, то вылетает https://stackoverflow.com/questions/214741/what-is-a-stackoverflowerror

Также некоторые могут вспомнить про MetaSpace)

3) MetaSpace, он же Class metadata. Хранит метаданные классов, загружаемые classloaders. А классов в Java много - стандартная библиотека rt.jar, Spring, другие используемые библиотеки, ваши собственные классы, генерация классов при работе программы.

До Java 8 классы хранились в области с названием PermGenSpace. Возможно кто-то еще помнит, а может и сталкивается до сих пор с OutOfMemoryError: PermGen space. (((

Отличия MetaSpace от PermGenSpace - неограниченный размер по умолчанию и то, что с ним наконец-то научился работать сборщик мусора. Хотя размер по умолчанию не ограничен, регулировать его при желании можно: XX:MaxMetaspaceSize и XX:CompressedClassSpaceMax. При желании можно получить и OutOfMemoryError: Metaspace)))

Оффтоп: почему опции две? В зависимости от включения UseCompressedOops и UseCompressedClassesPointers класс попадает либо в обычный MetaSpace, либо в MetaSpace для классов с сжатыми указателями. MaxMetaspaceSize включает в себя CompressedClassSpaceMax.

Про сжатые указатели https://habr.com/ru/post/440166/

Теперь пойдут области, покрытые мраком)

4) GC - область памяти, используемая Garbage Collector при работе. Занимает 3-4% от кучи. Также размер зависит от типа сборщика мусора.

5) JIT\Code - как известно, компилятор у Java довольно тупой, а вот JVM - умная) При работе она анализирует использование классов, выполняет разные оптимизации, а также компилирует часто используемые классы в нативный код. Процесс называется Just In Time Compilation. Именно в этой области скомпилированный код и хранится.

Размер регулируется ключом: XX:ReservedCodeCacheSize. Если установить слишком маленький размер - будет постоянная рекомпиляция, начнет страдать производительность. Больше опций, регулирующих процесс тут - https://docs.oracle.com/javase/8/embedded/develop-apps-platforms/codecache.htm

6) JIT\Compiler - собственно память, занимаемая компилятором.

Ну и самые кишочки... )))

7) Internal - память под внутренние нужны JVM, не упомянутые выше.

В частности здесь выделяются Direct ByteBuffers - https://docs.oracle.com/javase/8/docs/api/java/nio/ByteBuffer.html В случае объекта в heap, содержащего данные файла, ОС вначале копирует данные в свой низкоуровневый буфер, а потом JVM копирует данные к себе, то в случае Direct ByteBuffers этого можно избежать. Используются во многих высоконагруженных системах, в частности Kafka.

Размер настраивается через -XX:MaxDirectMemorySize=N. Нехватку памяти можно определить по "OutOfMemoryError: Direct buffer memory".

Т.к. ByteBuffer - объект, то сборщик мусора также умеет убирать и данные, выделенные в Direct ByteBuffers. Правда с некоторой задержкой, т.к. сами данные все же находятся не в куче, и механизм уборки получается чуть более сложный.

Сравнение скорости работы прямого и heap буфера с комментарием о том, что это всего лишь один из возможных случаев использования: https://elizarov.livejournal.com/20381.html

А увидеть все эти области можно запустив java процесс со специальным ключом XX:NativeMemoryTracking и используя утилиту jcmd из состава JDK.

Cм. детали тут https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr007.html

Запускать в ПРОДе с этой опцией нужно с осторожностью, overhead может быть 5-10%

Может показаться, что главное - это heap, все остальное по сравнению с ним - мелочи. Это не всегда так, см. исследование https://shipilev.net/jvm/anatomy-quarks/

Для иллюстрации вот как разместиться в памяти такой код:

class MainClass {

void method1() { //<- main

int variable1 = 1;

Class1 variable2 = new Class1();

variable2.method2();

}

}

class Class1 {

static Class2 classVariable4 = new Class2();

int instanceVariable5 = 0;

Class2 instanceVariable6 = new Class2();

void method2() {

int variable3 = 3;

}

}

class Class2 { }

Взял отсюда https://stackoverflow.com/questions/362740/java-memory-model-can-someone-explain-it/362804

#java #interview_question