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)

  1. Куча Java

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

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

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

    Объем памяти ГХ варьируется между алгоритмами ГХ. -XX:+UseSerialGC и -XX:+UseShenandoahGC имеют наименьшие издержки. G1 или CMS могут легко использовать около 10% от общего размера кучи.

  3. Кэш кода

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

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

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

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

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

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

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

Помимо прямых 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? когда оба компилятора отключены.

Ответ 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

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