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

119

Для моего приложения память, используемая процессом 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
Куча - это место, где размещаются объекты, однако у JVM есть много других областей памяти, включая общие библиотеки, прямые буферы памяти, стеки потоков, компоненты графического интерфейса, метапространство. Вам нужно посмотреть, насколько большой может быть JVM, и сделать предел достаточно высоким, чтобы вы предпочли, чтобы процесс умер, а не использовал его больше.
Питер Лоури
2
Похоже, сборщик мусора использует много памяти. Вместо этого вы можете попробовать использовать сборщик CMS. Похоже, что для метапространства + кода используется ~ 125 МБ, однако без сокращения базы кода вы вряд ли сможете сделать это меньше. Выделенное пространство близко к вашему пределу, поэтому неудивительно, что его убивают.
Питер Лоури
где / как установить конфигурацию -Xms и -Xmx?
Мик
1
Программа выполняет много файловых операций (например, создает файлы размером в гигабайт)? Если это так, вы должны знать, что это cgroupsдобавляет дисковый кеш к используемой памяти - даже если он обрабатывается ядром и невидим для пользовательской программы. (Имейте в виду, команды psи docker statsне
считайте

Ответы:

207

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

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

Итак, что требует памяти в процессе Java?

Части JVM (в основном отображаются с помощью отслеживания встроенной памяти)

  1. Куча Java

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

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

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

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

  3. Кэш кода

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

  4. составитель

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

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

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

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

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

  7. Потоки

    Стеки потоков также отвечают за использование оперативной памяти. Размер стека контролируется -Xss. По умолчанию 1M на поток, но, к счастью, все не так плохо. ОС выделяет страницы памяти лениво, т.е. при первом использовании, поэтому фактическое использование памяти будет намного меньше (обычно 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: Native Memory Tracking , pmap , jemalloc , async-profiler. .

Обновить

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

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

apangin
источник
1
Нет ли интернированных строк в куче с jdk7? ( bugs.java.com/bugdatabase/view_bug.do?bug_id=6962931 ) - может я ошибаюсь.
j-keck
5
@ j-keck Строковые объекты находятся в куче, но хеш-таблица (сегменты и записи со ссылками и хэш-кодами) находится в памяти вне кучи. Я изменил формулировку предложения, чтобы быть более точным. Спасибо, что указали.
apangin 05
Чтобы добавить к этому, даже если вы используете непрямые ByteBuffers, JVM будет выделять временные прямые буферы в собственной памяти без наложения ограничений на память. Ср evanjones.ca/java-bytebuffer-leak.html
. Senkfuss
17

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 + cache как использование памяти). Хорошее обсуждение и ссылки: Разница между размером резидентного набора (RSS) и общим объемом выделенной памяти Java (NMT) для JVM, работающей в контейнере Docker

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

Ян Гарадж
источник
Действительно лучше с -XX:MaxRam. Я думаю, что он все еще использует больше, чем указано в максимуме, но это лучше, спасибо!
Nicolas Henneaux
Возможно, вам действительно нужно больше памяти для этого экземпляра Java. Всего 15267 классов, 56 потоков.
Ян Гарадж
1
Вот более подробная информация, аргументы Java -Xmx128m -Xms128m -Xss228k -XX:MaxRAM=256m -XX:+UseSerialGC, производит Docker 428.5MiB / 600MiBи jcmd 58 VM.native_memory -> Native Memory Tracking: Total: reserved=1571296KB, committed=314316KB. JVM занимает около 300 МБ, а контейнеру требуется 430 МБ. Где 130 МБ между отчетами JVM и отчетами ОС?
Nicolas Henneaux
1
Добавлена ​​информация / ссылка о RSS-памяти.
Ян Гарадж
Предоставленный RSS находится внутри контейнера для процесса Java только ps -p 71 -o pcpu,rss,size,vsizeс процессом Java, имеющим pid 71. На самом деле -XX:MaxRamэто не помогло, но предоставленная вами ссылка помогает с последовательным GC.
Nicolas Henneaux
8

TL; DR

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

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

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


Детали для каждого компонента

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

-Xshare:off

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

-XX:+UseSerialGC

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

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

Николя Хено
источник
-1

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

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

adiian
источник
Использование jlinkдовольно ограничено, поскольку требует, чтобы приложение было модульным. Автоматический модуль не поддерживается, поэтому нет простого пути туда.
Nicolas Henneaux 03
-1

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

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

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

v_sukt
источник