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


118

У моєму застосуванні пам'ять, що використовується в процесі Java, набагато більше, ніж розмір купи.

У системі, де контейнери працюють, починаються проблеми з пам'яттю, оскільки контейнер займає набагато більше пам'яті, ніж розмір купи.

Розмір купи встановлюється на 128 МБ ( -Xmx128m -Xms128m), тоді як контейнер займає до 1 ГБ пам'яті. У нормальному стані йому потрібно 500 Мб. Якщо докер-контейнер має обмеження нижче (наприклад mem_limit=mem_limit=400MB), процес вбивається вбивцею пам'яті ОС.

Чи можете ви пояснити, чому процес Java використовує набагато більше пам'яті, ніж купа? Як правильно встановити розмір межі пам'яті Docker? Чи є спосіб зменшити спокій пам’яті пам’яті процесу Java?


Я збираю деякі подробиці про цю проблему за допомогою команди з відстеження пам'яті Native в 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 (всередині контейнера). Базується зображення Докера 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
Схоже, що GC використовує багато пам'яті. Ви можете спробувати використовувати колектор CMS. Схоже, для метапростору + код використовується ~ 125 Мб, проте, не скорочуючи базу коду, ви навряд чи зможете зробити це меншим. Виділений простір близько до вашої межі, тому не дивно, що його вбивають.
Пітер Лоурі

де / як ви встановлюєте конфігурацію -Xms та -Xmx?
Мік


1
Ви програмуєте виконувати багато файлових операцій (наприклад, створює файли у розмірі гігабайт)? Якщо це так, ви повинні знати, що cgroupsдодає дисковий кеш до використовуваної пам'яті - навіть якщо ним керує ядро ​​і він невидимий для програми користувача. ( psdocker stats
Майте на

Відповіді:


206

Віртуальна пам'ять, яка використовується Java-процесом, виходить далеко за рамки лише Java Heap. Ви знаєте, JVM включає багато підсистем: Garbage Collector, Class Class, JIT-компілятори тощо, і всі ці підсистеми потребують певної кількості оперативної пам’яті, щоб функціонувати.

JVM - не єдиний споживач оперативної пам'яті. Рідні бібліотеки (включаючи стандартну бібліотеку класів Java) також можуть виділяти нативну пам'ять. І це не буде видно навіть відстеженням Native Memory. Сам додаток Java також може використовувати позашляхову пам'ять за допомогою прямих ByteBuffers.

Отже, що займає пам'ять у процесі Java?

Запчастини JVM (в основному показані відстеженням Native Memory)

  1. Java Heap

    Найбільш очевидна частина. Тут живуть об’єкти 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(240 мм за замовчуванням). Вимкніть, -XX:-TieredCompilationщоб зменшити кількість скомпільованого коду, і, таким чином, використання кеш-коду.

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

    Сам компілятор JIT також потребує пам'яті, щоб виконувати свою роботу. Це може бути зменшено знову шляхом виключення багаторівневої компіляції або шляхом зменшення числа потоків компілятора: -XX:CICompilerCount.

  5. Завантаження класу

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

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

    Два основні хештелі JVM: таблиця Symbol містить імена, підписи, ідентифікатори тощо, а таблиця String містить посилання на інтерновані рядки. Якщо Native Tracking Memory відзначає значне використання пам'яті в таблиці String, це, ймовірно, означає, що програма надмірно дзвонить String.intern.

  7. Нитки

    Стопки ниток також відповідають за отримання оперативної пам'яті. Розмір стека контролюється за допомогою -Xss. За замовчуванням - 1 М на кожну нитку, але, на щастя, справи не такі вже й погані. ОС виділяє сторінки пам’яті ліниво, тобто при першому використанні, тому фактичне використання пам’яті буде значно нижчим (як правило, 80-200 Кб на стек потоку). Я написав сценарій, щоб оцінити, скільки RSS належить до стеків потоків Java.

    Є й інші частини JVM, які виділяють нативну пам'ять, але вони зазвичай не відіграють великої ролі в загальному споживанні пам'яті.

Прямі буфери

Додаток може явно вимагати виклику пам’яті, що не працює, зателефонувавши ByteBuffer.allocateDirect. Ліміт відключення за замовчуванням дорівнює -Xmx, але його можна перекрити -XX:MaxDirectMemorySize. Прямі ByteBuffers включаються в 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.

Один з можливих підходів до встановлення лімітів Докера - це перегляд фактичного використання пам'яті у "нормальному" стані процесу. Існують інструменти та методи розслідування проблем із споживанням пам’яті Java: відстеження Native Memory , pmap , jemalloc , async- profiler .

Оновлення

Ось запис моєї презентації Пам'ять пам'яті процесу Java .

У цьому відео я обговорюю, що може споживати пам'ять у процесі Java, як контролювати та обмежувати розмір певних областей пам’яті та як профілювати витоки рідної пам’яті у додатку Java.


1
Чи не інтерновані струни в купі з jdk7? ( bugs.java.com/bugdatabase/view_bug.do?bug_id=6962931 ) - можливо, я помиляюся.
j-keck

5
@ j-keck String-об'єкти знаходяться в купі, але хеш-таблиця (відра та записи із посиланнями та хеш-кодами) знаходиться у позагромовій пам'яті. Я переформулював речення, щоб бути більш точним. Дякуємо, що вказали.
apangin

щоб додати до цього, навіть якщо ви використовуєте непрямі ByteBuffers, JVM буде виділяти тимчасові прямі буфери в рідній пам'яті без обмежень пам'яті. Ср. evanjones.ca/java-bytebuffer-leak.html
Cpt. Сенкфус

16

https://developers.redhat.com/blog/2017/04/04/openjdk-and-containers/ :

Чому так, коли я вказую -Xmx = 1 г, мій JVM використовує більше пам'яті, ніж 1 Гб пам'яті?

Вказівка ​​-Xmx = 1g сповіщає JVM виділити купу 1gb. Це не говорить JVM обмежувати його повне використання пам'яті до 1 Гб. Є таблиці карт, кеш-кодів та всілякі інші структури даних із купи. Параметр, який ви використовуєте для визначення загального обсягу використання пам'яті -XX: MaxRAM. Майте на увазі, що при -XX: MaxRam = 500 м ваша купа буде приблизно 250 мб.

Java бачить розмір пам’яті хоста і не знає жодних обмежень пам’яті контейнера. Він не створює тиску в пам'яті, тому GC також не потребує звільнення використаної пам'яті. Сподіваюся XX:MaxRAM, допоможе вам зменшити слід пам’яті. В кінці кінців, ви можете налаштувати конфігурацію GC ( -XX:MinHeapFreeRatio, -XX:MaxHeapFreeRatio, ...)


Існує багато видів метрики пам'яті. Схоже, Docker повідомляє про розмір пам'яті RSS, який може відрізнятися від "прихильної" пам'яті, про яку повідомляється jcmd(старіші версії Docker звітують про RSS + кеш як використання пам'яті). Гарне обговорення та посилання: Різниця між розміром Resident Set (RSS) та загальною віддаченою пам'яттю Java (NMT) для JVM, що працює в контейнері Docker

(RSS) пам'ять може харчуватися також деякими іншими утилітами контейнера - оболонкою, менеджером процесів, ... Ми не знаємо, що ще працює в контейнері і як запускати процеси в контейнері.


І справді краще з -XX:MaxRam. Я думаю, він все ще використовує більше визначеного максимального, але це краще, дякую!
Ніколя Генно

Можливо, вам справді потрібно більше пам’яті для цього екземпляра 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 Мб. Де 130MB між звітом JVM та звітністю про ОС?
Ніколя Генно

1
Додана інформація / посилання про RSS-пам'ять.
Ян Гарадж

Наданий RSS знаходиться зсередини контейнера для процесу Java лише ps -p 71 -o pcpu,rss,size,vsizeз процесом Java, який має pid 71. Насправді -XX:MaxRamце не допомагало, але надане вами посилання допомагає при послідовній GC.
Ніколя Генно

8

TL; DR

Використання детальної пам’яті забезпечується деталізацією Native Memory Tracking (NMT) (в основному метадані коду та збирач сміття). На додаток до цього, компілятор Java та оптимізатор C1 / C2 споживають пам'ять, про яку не повідомляється в резюме.

Спад пам'яті можна зменшити за допомогою прапорців JVM (але є наслідки).

Розміщення контейнерів Docker необхідно здійснити шляхом тестування програми з очікуваним завантаженням.


Докладно про кожен компонент

Загальний клас простір може бути відключено всередині контейнера , так як класи не будуть передаватися іншим процесом віртуальної машини Java. Наступний прапор може бути використаний. Це видалить загальний простір класу (17 МБ).

-Xshare:off

Серія сміттєзбірників має мінімальний слід пам’яті за рахунок більш тривалого часу паузи під час переробки сміття (див. Порівняння Олексія Шипілева між GC на одному знімку ). Його можна ввімкнути за допомогою наступного прапора. Це може заощадити до використовуваного простору GC (48MB).

-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

Яві потрібно багато пам’яті. Сам 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

Якщо ви не хочете витрачати час на це, ви можете просто виділити більше пам'яті. Іноді це найкраще.


Використання jlinkє досить обмежуючим, оскільки вимагає модуляції програми. Автоматичний модуль не підтримується, тому немає простого шляху туди.
Ніколя Генно

-1

Як правильно встановити розмір межі пам'яті Docker? Перевірте додаток, спостерігаючи за ним деякий час. Для обмеження пам’яті контейнера спробуйте скористатися параметром -m, --memory bytes для команди docker run - або щось еквівалентне, якщо ви працюєте з ним інакше, як

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

не може відповісти на інші запитання.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.