Java использует намного больше памяти, чем размер кучи (или размер корректно ограничивает объем памяти Docker) - PullRequest
0 голосов
/ 23 ноября 2018

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

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

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

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


Я собираю некоторые подробности о проблеме, используя команду из Собственное отслеживание памяти в 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

Ответы [ 6 ]

0 голосов
/ 13 декабря 2018

Все ответы выше объясняют причину, по которой 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/

0 голосов
/ 05 декабря 2018

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

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

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

JVM-части (в основном это показывает Native Memory Tracking)

  1. Java Heap

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

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

    Структуры и алгоритмы GC требуют дополнительной памяти для управления кучей.Такими структурами являются Mark Bitmap, Mark Stack (для обхода графа объекта), Remembered Sets (для записи межрегиональных ссылок) и другие.Некоторые из них настраиваются напрямую, например, -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. Загрузка классов

    Метаданные класса (байт-коды метода, символы, постоянные пулы, аннотации и т. д.) хранятся в области вне кучи, называемой 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

Помимо прямых байтовых буферов тамможет быть 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 .

0 голосов
/ 04 декабря 2018

TL; DR

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

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

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


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

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

-Xshare:off

Серийный сборщик мусора имеет минимальный объем памяти за счет увеличения времени паузы при обработке сбора мусора (см. Сравнение Алексея Шипилёва между ГК в одной картине ).Это может быть включено с помощью следующего флага.Он может сэкономить до используемого пространства 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 for JVM содержится некоторая соответствующая информация о различных пространствах памяти.Oracle предоставляет некоторые подробности в документации по отслеживанию собственной памяти .Подробнее об уровне компиляции в расширенной политике компиляции и в отключении C2 уменьшите размер кэша кода в 5 раз.Некоторые сведения о Почему JVM сообщает о большем объеме выделенной памяти, чем размер резидентного набора процесса Linux? , когда оба компилятора отключены.

0 голосов
/ 04 декабря 2018

Как правильно определить ограничение памяти Docker? Проверьте приложение, отслеживая его некоторое время.Чтобы ограничить память контейнера, попробуйте использовать опцию -m, --memory bytes для команды запуска docker - или что-то эквивалентное, если вы запускаете его, в противном случае, например,

docker run -d --name my-container --memory 500m <iamge-name>

не может ответить на другие вопросы.

0 голосов
/ 04 декабря 2018

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) память также может быть съедена некоторымидругие утилиты в контейнере - оболочка, менеджер процессов, ... Мы не знаем, что еще выполняется в контейнере и как вы запускаете процессы в контейнере.

0 голосов
/ 01 декабря 2018

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

Начиная с java 9, у вас есть нечто, называемое project 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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...