Вопрос-ответ

Java using much more memory than heap size (or size correctly Docker memory limit)

Java использует намного больше памяти, чем размер кучи (или правильно определяет размер Docker memory limit)

Для моего приложения память, используемая процессом Java, намного больше, чем размер кучи.

Система, в которой запущены контейнеры, начинает испытывать проблемы с памятью, потому что контейнер занимает намного больше памяти, чем размер кучи.

Размер кучи установлен равным 128 МБ (-Xmx128m -Xms128m), в то время как контейнер занимает до 1 ГБ памяти. В нормальных условиях ему требуется 500 МБ. Если контейнер docker имеет ограничение ниже (например, mem_limit=mem_limit=400MB), процесс завершается из-за нехватки памяти в операционной системе.

Не могли бы вы объяснить, почему процесс Java использует гораздо больше памяти, чем куча? Как правильно установить размер ограничения памяти Docker? Есть ли способ уменьшить объем памяти, занимаемый процессом Java вне кучи?


Я собираю некоторые подробности о проблеме, используя команду из Native memory tracking в JVM.

Из хост-системы я получаю память, используемую контейнером.

$ docker stats --no-stream 9afcb62a26c8
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
9afcb62a26c8 xx-xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.0acbb46bb6fe3ae1b1c99aff3a6073bb7b7ecf85 0.93% 461MiB / 9.744GiB 4.62% 286MB / 7.92MB 157MB / 2.66GB 57

Изнутри контейнера я получаю память, используемую процессом.

$ ps -p 71 -o pcpu,rss,size,vsize
%CPU RSS SIZE VSZ
11.2 486040 580860 3814600

$ jcmd 71 VM.native_memory
71:

Native Memory Tracking:

Total: reserved=1631932KB, committed=367400KB
- Java Heap (reserved=131072KB, committed=131072KB)
(mmap: reserved=131072KB, committed=131072KB)

- Class (reserved=1120142KB, committed=79830KB)
(classes #15267)
( instance classes #14230, array classes #1037)
(malloc=1934KB #32977)
(mmap: reserved=1118208KB, committed=77896KB)
( Metadata: )
( reserved=69632KB, committed=68272KB)
( used=66725KB)
( free=1547KB)
( waste=0KB =0.00%)
( Class space:)
( reserved=1048576KB, committed=9624KB)
( used=8939KB)
( free=685KB)
( waste=0KB =0.00%)

- Thread (reserved=24786KB, committed=5294KB)
(thread #56)
(stack: reserved=24500KB, committed=5008KB)
(malloc=198KB #293)
(arena=88KB #110)

- Code (reserved=250635KB, committed=45907KB)
(malloc=2947KB #13459)
(mmap: reserved=247688KB, committed=42960KB)

- GC (reserved=48091KB, committed=48091KB)
(malloc=10439KB #18634)
(mmap: reserved=37652KB, committed=37652KB)

- Compiler (reserved=358KB, committed=358KB)
(malloc=249KB #1450)
(arena=109KB #5)

- Internal (reserved=1165KB, committed=1165KB)
(malloc=1125KB #3363)
(mmap: reserved=40KB, committed=40KB)

- Other (reserved=16696KB, committed=16696KB)
(malloc=16696KB #35)

- Symbol (reserved=15277KB, committed=15277KB)
(malloc=13543KB #180850)
(arena=1734KB #1)

- Native Memory Tracking (reserved=4436KB, committed=4436KB)
(malloc=378KB #5359)
(tracking overhead=4058KB)

- Shared class space (reserved=17144KB, committed=17144KB)
(mmap: reserved=17144KB, committed=17144KB)

- Arena Chunk (reserved=1850KB, committed=1850KB)
(malloc=1850KB)

- Logging (reserved=4KB, committed=4KB)
(malloc=4KB #179)

- Arguments (reserved=19KB, committed=19KB)
(malloc=19KB #512)

- Module (reserved=258KB, committed=258KB)
(malloc=258KB #2356)

$ cat /proc/71/smaps | grep Rss | cut -d: -f2 | tr -d " " | cut -f1 -dk | sort -n | awk '{ sum += $1 } END { print sum }'
491080

Приложение представляет собой веб-сервер, использующий Jetty / Jersey / CDI, упакованный в fat far объемом 36 МБ.

Используются следующие версии ОС и Java (внутри контейнера). Образ Docker основан на openjdk:11-jre-slim.

$ java -version
openjdk version "11" 2018-09-25
OpenJDK Runtime Environment (build 11+28-Debian-1)
OpenJDK 64-Bit Server VM (build 11+28-Debian-1, mixed mode, sharing)
$ uname -a
Linux service1 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2018 x86_64 GNU/Linux

https://gist.github.com/prasanthj/48e7063cac88eb396bc9961fb3149b58

Переведено автоматически
Ответ 1

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

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

Итак, что же занимает память в процессе Java?

Части JVM (в основном показаны при отслеживании встроенной памяти)

1. Java Heap

Самая очевидная часть. Здесь находятся объекты Java. Куча занимает до -Xmx объема памяти.

2. Сборщик мусора

Структурам и алгоритмам GC требуется дополнительная память для управления кучей. Этими структурами являются Mark Bitmap, Mark Stack (для обхода графа объектов), запоминаемые наборы (для записи межрегиональных ссылок) и другие. Некоторые из них настраиваются напрямую, например, -XX:MarkStackSizeMax, другие зависят от расположения кучи, например, чем больше области G1 (-XX:G1HeapRegionSize), тем меньше запоминаемые наборы.

Накладные расходы на GC-память различаются в зависимости от алгоритмов GC. -XX:+UseSerialGC и -XX:+UseShenandoahGC имеют наименьшие накладные расходы. G1 или CMS могут легко использовать около 10% от общего размера кучи.

3. Кэш кода

Содержит динамически генерируемый код: JIT-скомпилированные методы, интерпретатор и заглушки времени выполнения. Его размер ограничен -XX:ReservedCodeCacheSize (240 М по умолчанию). Отключите -XX:-TieredCompilation, чтобы уменьшить объем скомпилированного кода и, следовательно, использование кэша кода.

4. Компилятор

Самому JIT-компилятору также требуется память для выполнения своей работы. Это можно снова уменьшить, отключив многоуровневую компиляцию или уменьшив количество потоков компилятора: -XX:CICompilerCount.

5. Загрузка класса

Метаданные класса (байт-коды методов, символы, пулы констант, аннотации и т.д.) Хранятся вне кучи, называемой метапространством. Чем больше классов загружено - тем больше метапространства используется. Общее использование может быть ограничено -XX:MaxMetaspaceSize (неограниченно по умолчанию) и -XX:CompressedClassSpaceSize (1G по умолчанию).

6. Таблицы символов

Две основные хэш-таблицы JVM: таблица символов содержит имена, подписи, идентификаторы и т.д., А таблица строк содержит ссылки на интернированные строки. Если отслеживание встроенной памяти указывает на значительное использование памяти строковой таблицей, это, вероятно, означает, что приложение выполняет чрезмерные вызовы String.intern.

7. Потоки

Стеки потоков также отвечают за потребление оперативной памяти. Размер стека контролируется -Xss. По умолчанию 1 м на поток, но, к счастью, все не так плохо. ОС распределяет страницы памяти лениво, т. Е. При первом использовании, поэтому фактическое использование памяти будет намного ниже (обычно 80-200 КБ на стек потоков). Я написал скрипт, чтобы оценить, какая часть RSS принадлежит стекам потоков Java.

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

Прямые буферы

Приложение может явно запросить память вне кучи, вызвав ByteBuffer.allocateDirect. Ограничение вне кучи по умолчанию равно -Xmx, но его можно переопределить с помощью -XX:MaxDirectMemorySize. Прямые байт-буферы включены в Other раздел вывода NMT (или Internal до JDK 11).

Объем используемой напрямую памяти виден через JMX, например, в JConsole или Java Mission Control:

BufferPool MBean

Помимо прямых байт-буферов могут быть 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 debugging agent - также могут вызывать чрезмерное потребление памяти.

Этот ответ описывает, как профилировать выделение встроенной памяти с помощью 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.

Обновить

Вот запись моей презентации Объем памяти Java-процесса.

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

Ответ 2

https://developers.redhat.com/blog/2017/04/04/openjdk-and-containers /:


Почему, когда я указываю -Xmx = 1g, моя JVM использует больше памяти, чем 1 ГБ?


Указание -Xmx=1g указывает JVM выделить кучу объемом 1 ГБ. Это не говорит JVM ограничить использование всей памяти 1 ГБ. Существуют карточные таблицы, кэши кода и всевозможные другие структуры данных вне кучи. Параметр, который вы используете для указания общего использования памяти, равен -XX:MaxRAM. Имейте в виду, что с -XX:MaxRAM = 500m ваша куча будет составлять примерно 250 мб.


Java видит размер памяти хоста и не знает о каких-либо ограничениях памяти контейнера. Это не создает нехватки памяти, поэтому GC также не нужно освобождать используемую память. Я надеюсь, XX:MaxRAM это поможет вам уменьшить объем памяти. В конце концов, вы сможете настроить конфигурацию GC (-XX:MinHeapFreeRatio,-XX:MaxHeapFreeRatio, ...)


Существует множество типов показателей памяти. Docker, похоже, сообщает о размере памяти RSS, который может отличаться от "зафиксированной" памяти, о которой сообщает jcmd (более старые версии Docker сообщают об использовании памяти RSS + cache). Полезное обсуждение и ссылки: Разница между размером резидентного набора (RSS) и общим объемом выделенной памяти Java (NMT) для JVM, работающей в контейнере Docker

(RSS) память могут потреблять также некоторые другие утилиты в контейнере - оболочка, диспетчер процессов, ... Мы не знаем, что еще запущено в контейнере и как вы запускаете процессы в контейнере.

Ответ 3

TL; DR

Подробное использование памяти обеспечивается деталями отслеживания собственной памяти (NMT) (в основном метаданными кода и сборщиком мусора). В дополнение к этому компилятор Java и оптимизатор C1 / C2 потребляют память, не указанную в сводке.

Объем памяти может быть уменьшен с помощью флагов JVM (но есть последствия).

Определение размера контейнера Docker должно выполняться путем тестирования с ожидаемой загрузкой приложения.


Подробно о каждом компоненте

Пространство совместно используемых классов может быть отключено внутри контейнера, поскольку классы не будут совместно использоваться другим процессом JVM. Можно использовать следующий флаг. Это приведет к удалению общего пространства классов (17 МБ).

-Xshare:off

Последовательный сборщик мусора занимает минимум памяти за счет более длительной паузы во время обработки сбора мусора (см. Сравнение Алексея Шипилева с GC на одном рисунке). Это можно включить со следующим флагом. Это может сэкономить до используемого пространства GC (48 МБ).

-XX:+UseSerialGC

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

-XX:+TieredCompilation -XX:TieredStopAtLevel=1

Пространство кода сокращено на 20 МБ. Более того, объем памяти за пределами JVM сокращен на 80 МБ (разница между пространством NMT и пространством RSS). Оптимизирующему компилятору C2 требуется 100 МБ.

Компиляторы C1 и C2 могут быть отключены с помощью следующего флага.

-Xint

Объем памяти за пределами JVM теперь меньше общего выделенного пространства. Объем кода сокращен на 43 МБ. Будьте осторожны, это серьезно влияет на производительность приложения. Отключение компиляторов C1 и C2 уменьшает объем используемой памяти на 170 МБ.

Использование компилятора Graal VM (замена C2) приводит к немного меньшему объему памяти. Это увеличивает на 20 МБ объем памяти кода и уменьшает на 60 МБ объем внешней памяти JVM.

В статье Управление памятью Java для JVM приводится некоторая соответствующая информация о различных пространствах памяти. Oracle предоставляет некоторые подробности в документации по отслеживанию собственной памяти. Подробнее об уровне компиляции читайте в расширенной политике компиляции и в отключите C2, чтобы уменьшить размер кэша кода в 5 раз. Некоторые подробности о почему JVM сообщает о большем объеме выделенной памяти, чем размер резидентного набора процесса Linux? когда оба компилятора отключены.

Ответ 4

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

Начиная с java 9, у вас есть нечто, называемое project Jigsaw, которое может уменьшить объем памяти, используемой при запуске java-приложения (вместе со временем запуска). Project jigsaw и новая модульная система не обязательно создавались для уменьшения необходимой памяти, но если это важно, вы можете попробовать.

Вы можете взглянуть на этот пример: https://steveperkins.com/using-java-9-modularization-to-ship-zero-dependency-native-apps / . При использовании модульной системы это привело к созданию приложения CLI объемом 21 МБ (со встроенным JRE). JRE занимает более 200 МБ. Это должно привести к уменьшению выделяемой памяти при запуске приложения (множество неиспользуемых классов JRE больше не будут загружаться).

Вот еще одно приятное руководство: https://www.baeldung.com/project-jigsaw-java-modularity

Если вы не хотите тратить время на это, вы можете просто выделить больше памяти. Иногда это лучшее.

java jvm