Виртуальная память, используемая процессом Java, выходит далеко за рамки Java Heap.Вы знаете, JVM включает в себя множество подсистем: сборщик мусора, загрузка классов, JIT-компиляторы и т. Д., И все эти подсистемы требуют определенного объема ОЗУ для работы.
JVM не единственный потребитель ОЗУ.Собственные библиотеки (включая стандартную библиотеку классов Java) также могут выделять собственную память.И это не будет даже видно для Native Memory Tracking.Само Java-приложение может также использовать память вне кучи с помощью прямых байтовых буферов.
Так что же занимает память в Java-процессе?
JVM-части (в основном это показывает Native Memory Tracking)
Java Heap
Самая очевидная часть.Здесь живут объекты Java.Куча занимает до -Xmx
объема памяти.
Сборщик мусора
Структуры и алгоритмы GC требуют дополнительной памяти для управления кучей.Такими структурами являются Mark Bitmap, Mark Stack (для обхода графа объекта), Remembered Sets (для записи межрегиональных ссылок) и другие.Некоторые из них настраиваются напрямую, например, -XX:MarkStackSizeMax
, другие зависят от компоновки кучи, например, чем больше области G1 (-XX:G1HeapRegionSize
), тем меньше запоминаемые наборы.
Расход памяти GC варьируется между алгоритмами GC.-XX:+UseSerialGC
и -XX:+UseShenandoahGC
имеют наименьшие накладные расходы.G1 или CMS могут легко использовать около 10% от общего размера кучи.
Кэш кода
Содержит динамически сгенерированный код: JIT-скомпилированные методы, интерпретатор и заглушки во время выполнения,Его размер ограничен -XX:ReservedCodeCacheSize
(по умолчанию 240М).Отключите -XX:-TieredCompilation
, чтобы уменьшить объем скомпилированного кода и, следовательно, использование кэша кода.
Компилятор
Самому JIT-компилятору также требуется память, чтобы выполнять свою работу.Это можно снова уменьшить, отключив многоуровневую компиляцию или уменьшив количество потоков компилятора: -XX:CICompilerCount
.
Загрузка классов
Метаданные класса (байт-коды метода, символы, постоянные пулы, аннотации и т. д.) хранятся в области вне кучи, называемой Metaspace.Чем больше классов загружено - тем больше метапространства используется.Общее использование может быть ограничено -XX:MaxMetaspaceSize
(по умолчанию не ограничено) и -XX:CompressedClassSpaceSize
(по умолчанию 1G).
Таблицы символов
Две основные хеш-таблицы JVM: таблица символов содержит имена, подписи, идентификаторы и т. д., а таблица String содержит ссылки на интернированные строки.Если отслеживание собственной памяти указывает на значительное использование памяти таблицей строк, это, вероятно, означает, что приложение чрезмерно вызывает String.intern
.
Потоки
Стеки потоков также отвечают за взятиеБАРАН.Размер стека контролируется -Xss
.По умолчанию 1М на поток, но, к счастью, все не так плохо.ОС распределяет страницы памяти лениво, т. Е. При первом использовании, поэтому фактическое использование памяти будет намного ниже (обычно 80-200 КБ на стек потоков).Я написал сценарий , чтобы оценить, сколько RSS принадлежит стекам потоков Java.
Существуют другие части JVM, которые выделяют собственную память, но обычно они не играют большой роли в общей памяти.потребление.
Прямые буферы
Приложение может явно запрашивать память вне кучи, вызывая ByteBuffer.allocateDirect
.По умолчанию предел нехватки кучи равен -Xmx
, но его можно переопределить с помощью -XX:MaxDirectMemorySize
.Прямые байтовые буферы включены в секцию Other
вывода NMT (или Internal
до JDK 11).
Объем используемой прямой памяти виден через JMX, например, в JConsole или Java Mission Control:
Помимо прямых байтовых буферов тамможет быть MappedByteBuffers
- файлы, сопоставленные с виртуальной памятью процесса.NMT не отслеживает их, однако MappedByteBuffers также может занимать физическую память.И нет простого способа ограничить, сколько они могут взять.Вы можете просто увидеть фактическое использование, посмотрев карту памяти процесса: pmap -x <pid>
Address Kbytes RSS Dirty Mode Mapping
...
00007f2b3e557000 39592 32956 0 r--s- some-file-17405-Index.db
00007f2b40c01000 39600 33092 0 r--s- some-file-17404-Index.db
^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
Собственные библиотеки
Код JNI, загруженный System.loadLibrary
, может выделить столько памяти вне кучи, сколько ему нужно, без контроля со стороны JVM.Это также касается стандартной библиотеки классов Java.В частности, незакрытые ресурсы Java могут стать источником утечки памяти.Типичными примерами являются ZipInputStream
или DirectoryStream
.
Агенты JVMTI, в частности, jdwp
агент отладки - также может вызвать чрезмерное потребление памяти.
Этот ответ описывает, как профилировать распределение собственной памяти с помощью async-profiler .
Проблемы с распределителем
Процесс обычно запрашивает собственную память либо непосредственно из ОС (с помощью системного вызова mmap
)или используя malloc
- стандартный распределитель libc.В свою очередь, malloc
запрашивает большие порции памяти из ОС, используя mmap
, а затем управляет этими порциями в соответствии со своим собственным алгоритмом выделения.Проблема в том, что этот алгоритм может привести к фрагментации и чрезмерному использованию виртуальной памяти .
jemalloc
, альтернативному распределителю, который часто выглядит умнее обычного libc malloc
, поэтому переключение на jemalloc
может привести к уменьшению занимаемой площади бесплатно.
Заключение
Не существует гарантированного способа оценить полное использование памяти процессом Java, поскольку их слишком много.Факторы, которые следует учитывать.
Total memory = Heap + Code Cache + Metaspace + Symbol tables +
Other JVM structures + Thread stacks +
Direct buffers + Mapped files +
Native Libraries + Malloc overhead + ...
Можно уменьшить или ограничить определенные области памяти (например, кэш кода) с помощью флагов JVM, но многие другие вообще не контролируются JVM.
ОдинВозможный подход к настройке пределов Docker состоял бы в том, чтобы наблюдать за фактическим использованием памяти в «нормальном» состоянии процесса.Существуют инструменты и методы для исследования проблем с использованием памяти Java: Отслеживание собственной памяти , pmap , jemalloc , async-profiler .