Java использует гораздо больше памяти, чем размер кучи (или правильно размер памяти Docker)
Для моего приложения память, используемая процессом Java, намного больше, чем размер кучи.
Система, в которой работают контейнеры, начинает испытывать проблемы с памятью, поскольку контейнер занимает гораздо больше памяти, чем размер кучи.
Размер кучи установлен на 128 МБ (-Xmx128m -Xms128m
), в то время как контейнер занимает до 1 ГБ памяти. При нормальных условиях требуется 500 МБ. Если в контейнере mem_limit=mem_limit=400MB
установлен предел ниже (например, mem_limit=mem_limit=400MB
), процесс уничтожается из-за 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
Самая очевидная часть. Здесь живут объекты 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: Отслеживание собственной памяти, pmap, jemalloc, async-profiler.
Обновление
Вот запись моей презентации Память процесса Jav.
В этом видео я расскажу о том, что может потреблять память в процессе 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 = 500 м ваша куча будет примерно 250 МБ.
Java видит размер памяти хоста и не знает об ограничениях памяти контейнера. Это не создает нагрузку на память, поэтому GC также не нужно освобождать использованную память. Я надеюсь, что XX:MaxRAM
поможет вам уменьшить объем памяти. В конце концов, вы можете настроить конфигурацию GC (-XX:MinHeapFreeRatio
, -XX:MaxHeapFreeRatio
,...)
Существует много типов метрик памяти. Docker, похоже, сообщает об объеме памяти RSS, который может отличаться от "выделенной" памяти, сообщаемой jcmd
(более ранние версии Docker сообщают об использовании кэша RSS + как использование памяти). Хорошее обсуждение и ссылки: Разница между размером резидентного набора (RSS) и общей выделенной памятью Java (NMT) для JVM, работающей в контейнере Docker
(RSS) память может быть съедена также некоторыми другими утилитами в контейнере - оболочкой, менеджером процессов,... Мы не знаем, что еще выполняется в контейнере и как вы запускаете процессы в контейнере.
Ответ 3
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 Мбайт пространства памяти кода и уменьшает 60 МБ из-за пределов JVM-памяти.
В статье " Управление памятью Java для JVM" содержится некоторая релевантная информация о различных пространствах памяти. Oracle предоставляет некоторые сведения о документации по отслеживанию родной памяти. Более подробная информация о уровне компиляции в расширенной политике компиляции и отключении C2 уменьшает размер кеша кода в 5 раз. Некоторые подробности о том, почему JVM сообщает о большей памяти, чем размер резидентного набора для Linux? когда оба компилятора отключены.
Ответ 4
Все ответы выше объясняют причину, по которой 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/
Ответ 5
Как правильно определить предел памяти Docker? Проверьте приложение, проверив его некоторое время. Чтобы ограничить память контейнера, попробуйте использовать -m, - -m параметр emory bytes для команды запуска docker - или что-то эквивалентное, если вы используете его иначе, как
docker run -d --name my-container --memory 500m <iamge-name>
не может отвечать на другие вопросы.
Ответ 6
Java требует много памяти. Самому JVM нужно много памяти для запуска. Куча - это память, доступная внутри виртуальной машины, доступная вашему приложению. Поскольку JVM - это большой пакет, упакованный со всеми положительными эффектами, для загрузки требуется много памяти.
Начиная с java 9 у вас есть что-то, называемое проект Jigsaw, что может уменьшить память, используемую при запуске приложения Java (вместе со временем начала). Проект головоломки и новая модульная система не обязательно создавались для уменьшения необходимой памяти, но если это важно, вы можете попробовать.
Вы можете взглянуть на этот пример: 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
Если вы не хотите тратить время на это, вы можете просто выделить больше памяти. Иногда это лучше.