Java использует намного больше памяти, чем размер кучи (или размер корректно ограничивает объем памяти Docker)
Для моего приложения память, используемая процессом Java, намного больше, чем размер кучи.
Система, в которой работают контейнеры, начинает испытывать проблемы с памятью, поскольку контейнер занимает гораздо больше памяти, чем размер кучи.
Размер кучи установлен на 128 МБ (-Xmx128m -Xms128m
) в то время как контейнер занимает до 1 ГБ памяти. При нормальных условиях требуется 500 МБ. Если док-контейнер имеет ограничение ниже (например, 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, встроенный в большой далекий 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 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
), поменьше запоминаются наборы.Объем памяти ГХ варьируется между алгоритмами ГХ.
-XX:+UseSerialGC
а также-XX:+UseShenandoahGC
имеют наименьшие накладные расходы. G1 или CMS могут легко использовать около 10% от общего размера кучи.Кэш кода
Содержит динамически сгенерированный код: JIT-скомпилированные методы, интерпретатор и заглушки во время выполнения. Его размер ограничен
-XX:ReservedCodeCacheSize
(По умолчанию 240M). Выключи-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:
Помимо прямых ByteBuffers могут быть 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: Native Memory Tracking, pmap, jemalloc, async-profiler.
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=500 м ваша куча будет примерно 250 МБ.
Java видит размер памяти хоста и не знает об ограничениях памяти контейнера. Это не создает нагрузку на память, поэтому GC также не нужно освобождать использованную память. я надеюсь XX:MaxRAM
поможет вам уменьшить объем памяти. В конце концов, вы можете настроить конфигурацию GC (-XX:MinHeapFreeRatio
,-XX:MaxHeapFreeRatio
...)
Существует много типов метрик памяти. Docker, похоже, сообщает об объеме памяти RSS, который может отличаться от "выделенной" памяти, сообщаемой jcmd
(более старые версии Docker сообщают об использовании RSS+ кеша как памяти). Хорошее обсуждение и ссылки: Разница между размером резидентного набора (RSS) и общей выделенной памятью Java (NMT) для JVM, работающей в контейнере Docker
(RSS) память может быть съедена также некоторыми другими утилитами в контейнере - оболочкой, менеджером процессов, ... Мы не знаем, что еще выполняется в контейнере и как вы запускаете процессы в контейнере.
TL;DR
Подробное использование памяти обеспечивается деталями Native Memory Tracking (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 МБ и уменьшает объем внешней памяти JVM на 60 МБ.
Статья Java Memory Management для JVM предоставляет некоторую соответствующую информацию о различных пространствах памяти. Oracle предоставляет некоторые подробности в документации по отслеживанию собственной памяти. Дополнительные сведения об уровне компиляции в расширенной политике компиляции и при отключении C2 уменьшают размер кэша кода в 5 раз. Некоторые сведения о том, почему JVM сообщает о большей выделенной памяти, чем размер резидентного набора процесса Linux? когда оба компилятора отключены.
Java требует много памяти. Сама JVM нуждается в большом количестве памяти для запуска. Куча - это память, доступная внутри виртуальной машины, доступная вашему приложению. Поскольку JVM - это большой пакет со всеми возможными вкусностями, для загрузки требуется много памяти.
Начиная с java 9 у вас есть что-то, называемое Jigsaw проекта, которое может уменьшить объем памяти, используемой при запуске приложения java (вместе со временем запуска). Project jigsaw and a new module system were not necessarily created to reduce the necessary memory, but if it's important you can give a try.
You can take a look at this example: https://steveperkins.com/using-java-9-modularization-to-ship-zero-dependency-native-apps/. By using the module system it resulted in CLI application of 21MB(with JRE embeded). JRE takes more than 200mb. That should translate to less allocated memory when the application is up(a lot of unused JRE classes will no longer be loaded).
Here is another nice tutorial: https://www.baeldung.com/project-jigsaw-java-modularity
If you don't want to spend time with this you can simply get allocate more memory. Sometimes it's the best.
Все ответы выше объясняют причину, по которой JVM занимает так много памяти, но, возможно, вам больше всего нужно решение, эти статьи помогут:
- https://blogs.oracle.com/java-platform-group/java-se-support-for-docker-cpu-and-memory-limits
- https://royvanrijn.com/blog/2018/05/java-and-docker-memory-limits/
Как правильно определить лимит памяти Docker? Проверьте приложение, отслеживая его в течение некоторого времени. Чтобы ограничить память контейнера, попробуйте использовать опцию -m, --memory bytes для команды docker run - или что-то эквивалентное, если вы запускаете его иначе:
docker run -d --name my-container --memory 500m <iamge-name>
не могу ответить на другие вопросы.