Первоначально это было опубликовано в моем собственном блоге несколько лет назад, но я предпочел бы продолжить серию здесь, так что, перепост.
О чем этот пост?
Цель этого сообщения – дать обзор областей памяти кучи и не кучи JVM – с небольшим введением в обе области, а также показать, что происходит в случае проблемы с памятью кучи/не кучи в контейнере docker
. Я предполагаю некоторые базовые знания Java, JVM, docker и linux. Вам понадобятся docker и openjdk 8, установленные в системе Linux (я использовал ubuntu 16.04 для написания этого поста).
Для начала я собираюсь сделать все очень просто. Давайте создадим программу, которая печатает “Привет, мир!” и ждет вечно:
// HelloWorld.java public class HelloWorld { public static void main(String[] args) throws Exception { System.out.println("Hello world!"); System.in.read(); } }
Теперь, простой файл докера:
FROM openjdk:8-jdk ADD HelloWorld.java . RUN javac HelloWorld.java ENTRYPOINT java HelloWorld
С помощью этого мы можем создать и запустить наше приложение в контейнере:
$ docker build --tag jvm-test . $ docker run -ti --rm --name hello-jvm jvm-test Hello world!
Вы можете использовать CTRL-C, чтобы убить контейнер, когда закончите. Итак, теперь у нас запущена простая программа, что мы можем сделать? Давайте проанализируем JVM.
Базовый анализ JVM
Давайте получим список объектов, которые у нас есть в куче в нашем приложении. Во-первых, войдите в контейнер (предполагая, что он все еще работает сверху) и получите PID процессов JVM.
$ docker exec -ti hello-jvm bash root@5f20ae043968:/ $ ps aux|grep [j]ava root 1 0.1 0.0 4292 708 pts/0 Ss+ 12:27 0:00 /bin/sh -c java HelloWorld root 7 0.2 0.1 6877428 23756 pts/0 Sl+ 12:27 0:00 java HelloWorld
Из вышесказанного мы видим, что PID равен 7. Для анализа openjdk поставляется с рядом инструментов. карта
является одним из таких инструментов, который позволяет нам просматривать кучу информации о процессе JVM. Чтобы получить список объектов, их количество экземпляров и место, которое они занимают в куче, вы можете использовать jmap -histo
.
root@5f20ae043968:/ $ jmap -histo 7 num #instances #bytes class name --------------------------------------------------- 1: 422 2256744 [I 2: 1600 141520 [C 3: 364 58560 [B 4: 470 53544 java.lang.Class 5: 1204 28896 java.lang.String 6: 551 28152 [Ljava.lang.Object; 7: 110 7920 java.lang.reflect.Field 8: 258 4128 java.lang.Integer 9: 97 3880 java.lang.ref.SoftReference 10: 111 3552 java.util.Hashtable$Entry 11: 133 3192 java.lang.StringBuilder 12: 8 3008 java.lang.Thread 13: 75 2400 java.io.File 14: 54 2080 [Ljava.lang.String; 15: 38 1824 sun.util.locale.LocaleObjectCache$CacheEntry 16: 12 1760 [Ljava.util.Hashtable$Entry; 17: 55 1760 java.util.concurrent.ConcurrentHashMap$Node 18: 27 1728 java.net.URL 19: 20 1600 [S ... 222: 1 16 sun.reflect.ReflectionFactory Total 6583 2642792
Как вы можете видеть выше, для нашей простой программы HelloWorld существует 6583 экземпляра смеси из 222 различных классов, занимающих более 2,6 МБ кучи! Когда я впервые увидел это, у меня возникло много вопросов – что такое [Я
, почему существует java.lang. Строка
и [[Java.язык. Строка
?
Что это за занятия?
Однобуквенные имена классов, которые вы видите выше, все задокументированы в Class.getName() .
Z | логический |
B | байт |
C | обуглить |
L*название класса* | класс/интерфейс |
D | двойной |
F | плыть |
I | инт |
J | длинный |
S | короткий |
Если вы оглянетесь на вывод map
, то все первые несколько экземпляров имеют [
добавляя к ним префиксы – например, [I
. [
обозначает 1-мерный массив типа, исходящий из него – [I
обозначает массив int
например новый int[3]
. [[I
обозначает двумерный массив, новый int[2][3]
и так далее. Также в выводе map
выше были примеры [Л.ява.язык. Строка
, которая является просто массивом строк – новая строка[3]
.
Чтобы убедиться в этом самому:
// InstanceName.java public class InstanceName { public static void main(String[] args) throws Exception { int[] is = new int[3]; System.out.println(is.getClass().getName()); boolean[][][] bs = new boolean[2][5][4]; System.out.println(bs.getClass().getName()); String[] ss = new String[3]; System.out.println(ss.getClass().getName()); } }
Компилируя и запуская это, мы получаем:
$ javac InstanceName.java $ java InstanceName [I [[[Z [Ljava.lang.String;
Это краткий обзор одного из способов взглянуть на то, что загружено в кучу. Ранее я упоминал другие области памяти в JVM, что это такое?
JVM можно разделить на множество различных сегментов памяти (сегменты/регионы/области, я буду использовать эти слова взаимозаменяемо, но в целом они означают одно и то же), если мы сначала рассмотрим представление высокого уровня, у нас есть два сегмента – память, используемая для объектов в куче и не-кучная память.
Если мы увеличим масштаб, в куче есть разные области, о которых мы можем говорить, в зависимости от того, что мы хотим обсудить – есть пространство Эдема, где изначально создается большинство новых объектов, пространство выживших, куда попадают объекты, если они выживают в сборке мусора в пространстве Эдема (GC), и Старое поколение, которое содержит объекты, которые некоторое время жили в пространстве выживших. В частности, он содержит объекты, которые были инициализированы – например, List<Строка> ArrayList<Строка>();
создаст объект ArrayList
в куче, и s
укажет на это.
В предыдущем разделе я пробежался по тому, какие объекты загружаются в кучу для нашей программы HelloWorld, так что насчет памяти без кучи?
Память без кучи
Если вы когда-либо писали нетривиальное java-приложение с jdk8, вы, вероятно, слышали о Metaspace . Это пример памяти без кучи. Именно там JVM будет хранить определения классов, статические переменные, методы, загрузчики классов и другие метаданные. Но есть много других областей памяти, не связанных с кучей, которые будет использовать JVM. Давайте перечислим их!
Для этого сначала нам нужно включить отслеживание собственной памяти в нашем приложении java:
FROM openjdk:8-jdk ADD HelloWorld.java . RUN cat HelloWorld.java RUN javac HelloWorld.java ENTRYPOINT java -XX:NativeMemoryTracking=detail HelloWorld
Теперь постройте и запустите заново:
$ docker build --tag jvm-test . $ docker run -ti --rm --name hello-jvm jvm-test Hello world!
В другом терминале запустите в контейнер и получите сводку об общем использовании памяти с помощью jcmd
‘s VM.native_memory
команда:
$ docker exec --privileged -ti hello-jvm bash root@aa5ae77e1305:/ $ jcmd 33 sun.tools.jcmd.JCmd 7 HelloWorld root@aa5ae77e1305:/ $ jcmd 7 VM.native_memory summary 7: Native Memory Tracking: Total: reserved=5576143KB, committed=1117747KB - Java Heap (reserved=4069376KB, committed=920064KB) (mmap: reserved=4069376KB, committed=920064KB) - Class (reserved=1066121KB, committed=14217KB) (classes #405) (malloc=9353KB #178) (mmap: reserved=1056768KB, committed=4864KB) - Thread (reserved=20646KB, committed=20646KB) (thread #21) (stack: reserved=20560KB, committed=20560KB) (malloc=62KB #110) (arena=23KB #40) - Code (reserved=249655KB, committed=2591KB) (malloc=55KB #346) (mmap: reserved=249600KB, committed=2536KB) - GC (reserved=159063KB, committed=148947KB) (malloc=10383KB #129) (mmap: reserved=148680KB, committed=138564KB) - Compiler (reserved=134KB, committed=134KB) (malloc=3KB #37) (arena=131KB #3) - Internal (reserved=9455KB, committed=9455KB) (malloc=9423KB #1417) (mmap: reserved=32KB, committed=32KB) - Symbol (reserved=1358KB, committed=1358KB) (malloc=902KB #85) (arena=456KB #1) - Native Memory Tracking (reserved=161KB, committed=161KB) (malloc=99KB #1559) (tracking overhead=61KB) - Arena Chunk (reserved=175KB, committed=175KB) (malloc=175KB)
Гораздо больше регионов, чем просто куча! Наша программа “Привет, мир” только что стала еще более сложной…
Что все это значит? 1
- Куча Java : куча памяти.
- Класс : является метапространством регион, о котором мы говорили ранее.
- Нить : является ли пространство, занимаемое потоками на этом JVM.
- Код : является кэшем кода – он используется JIT для кэширования скомпилированного кода.
- GC : пространство, используемое сборщиком мусора.
- Компилятор : пространство, используемое JIT при генерации кода.
- Символы : это для символов, под которые, как я полагаю, подпадают имена полей, подписи методов. 2
- Отслеживание собственной памяти : память, используемая самим трекером собственной памяти.
- Кусок арены : не совсем уверен, для чего это используется. 3
Практические проблемы с памятью
Хорошо, так почему же вас должно волновать что-то из вышеперечисленного? Давайте создадим приложение, которое съедает тонну памяти.
// MemEater.java import java.util.Vector; public class MemEater { public static final void main(String[] args) throws Exception { Vectorv = new Vector (); for (int i = 0; i < 400; i++) { byte[] b = new byte[1048576]; // allocate 1 MiB v.add(b); } System.out.println(v.size()); Thread.sleep(10000); } }
Это создаст Вектор
, который содержит 400 байтовых массивов размером 1 Мбайт 4 , так что это будет использовать ~400 МБ памяти в куче. Затем он будет спать в течение 10 секунд, чтобы мы могли легко использовать память во время его работы. Давайте ограничим объем кучи 450 Мбайт и запустим это локально, чтобы увидеть фактическое использование памяти процессом. Размер резидентного набора RSS 5 вот как это измеряется, обратите внимание, что это значение также содержит страницы, отображенные из общей памяти , но мы можем замаскировать это для этого поста.
Итак, давайте скомпилируем наше приложение, запустим в фоновом режиме и получим его RSS:
$ javac MemEater.java $ nohup java -Xms450M -Xmx450M MemEater & $ ps aux | awk 'NR==1; /[M]emEater/' USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND chaospie 18019 10.5 3.0 3138368 494448 pts/19 Sl 16:06 0:00 java -Xms450M -Xmx450M MemEater
В общей сложности для запуска процесса JVM требуется около 500 мбайт (RSS составляет 494448 КБ). Что произойдет, если мы установим размер кучи меньше необходимого?
$ java -Xms400M -Xmx400M MemEater Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at MemEater.main(MemEater.java:7)
Если вы раньше использовали java (или любой другой язык JVM), вы, скорее всего, сталкивались с этим. Это означает, что в JVM не хватило места в куче для размещения объектов. Существует довольно много других типов OutOfMemoryError
JVM может вызывать в определенных ситуациях 6 , но я не буду сейчас вдаваться в подробности.
Теперь мы знаем, что произойдет, если в JVM недостаточно места в куче, как насчет случая, когда вы работаете в контейнере и достигли общего предела памяти для этого контейнера?
Самый простой способ воспроизвести это – упаковать нашу программу MemEater
в образ docker и запустить ее с меньшим объемом памяти, чем ей нужно.
FROM openjdk:8-jdk ADD MemEater.java . RUN cat MemEater.java RUN javac MemEater.java ENTRYPOINT java -Xms450M -Xmx450M MemEater
Опять же, нам нужно создать образ. Однако на этот раз, когда мы работаем, мы ограничиваем объем памяти, который может использовать контейнер, до 5 м:
$ docker build --tag jvm-test . $ docker run -ti --rm --memory 5M --memory-swappiness 0 --name memeater jvm-test WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap. Killed
Через несколько секунд вы должны увидеть вывод выше, Убит
. Что случилось? Прежде чем мы погрузимся в это, давайте взглянем на --память
и --возможность замены памяти
флаги, используемые докером
.
Ограничение памяти с помощью docker
Давайте на секунду отвлечемся и посмотрим на два флага настройки, которые я использовал выше для управления настройками памяти 7 . Во-первых, чтобы эти флаги работали, в вашем ядре должна быть включена поддержка cgroup и установлены следующие параметры загрузки (при условии, что grub
):
$ cat /etc/default/grub ... GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1" ...
--память
устанавливает верхнюю границу для суммы использования памяти всеми процессами в контейнере, наименьшее значение, которое может быть 4 МБ, выше мы устанавливаем значение 5 м, что равно 5 МБ. Когда это задано, контейнер cgroup
память.limit_in_bytes
устанавливается в значение. Я не могу найти код, который делает это в docker
, однако мы можем видеть это следующим образом:
$ docker run -d --rm --memory 500M --memory-swappiness 0 --name memeater jvm-test WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap. 812dbc3417eacdaf221c2f0c93ceab41f7626dca17f959298a5700358f931897 $ CONTAINER_ID=`docker ps --no-trunc | awk '{if (NR!=1) print $1}'` $ echo $CONTAINER_ID 812dbc3417eacdaf221c2f0c93ceab41f7626dca17f959298a5700358f931897 $ cat /sys/fs/cgroup/memory/docker/${CONTAINER_ID}/memory.swappiness 0 $ cat /sys/fs/cgroup/memory/docker/${CONTAINER_ID}/memory.limit_in_bytes 524288000 # Again, this time without limits to see the difference $ docker run -d --rm --name memeater jvm-test d3e25423814ee1d79759aa87a83d416d63bdb316a305e390c2b8b98777484822 $ CONTAINER_ID=`docker ps --no-trunc | awk '{if (NR!=1) print $1}'` $ echo $CONTAINER_ID d3e25423814ee1d79759aa87a83d416d63bdb316a305e390c2b8b98777484822 $ cat /sys/fs/cgroup/memory/docker/${CONTAINER_ID}/memory.swappiness 60 $ cat /sys/fs/cgroup/memory/docker/${CONTAINER_ID}/memory.limit_in_bytes 9223372036854771712
Обратите внимание на ПРЕДУПРЕЖДЕНИЕ
, я не совсем уверен, почему это появляется, когда включена поддержка подкачки, и, похоже, работает. Ты можешь пока не обращать на это внимания.
---- память-подкачка
задает подкачка уровень иерархии cgroup, в которой выполняется контейнер. Это напрямую связано с настройкой cgroup memory.swappiness (по крайней мере, в версии 17.12 docker 8 ) как видно выше. Установка этого значения в 0 отключает обмен для контейнера.
Что убивает контейнер?
Итак, почему контейнер был убит? Давайте запустим его еще раз:
$ docker run -ti --rm --memory 5M --memory-swappiness 0 --name memeater jvm-test WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap. Killed
Чтобы увидеть причину этого убийства, запустите journalctl-k
и найдите убийцу оом
, вы должны увидеть журналы, подобные следующим:
$ journalctl -k ... Feb 18 17:34:47 kernel: java invoked oom-killer: gfp_mask=0x14000c0(GFP_KERNEL), nodemask=(null), order=0, oom_score_adj=0 Feb 18 17:34:47 kernel: java cpuset=35f18c48d432510c76e76f2e7a962e64a1372de1dc4abd830417263907bea6e0 mems_allowed=0 Feb 18 17:34:47 kernel: CPU: 0 PID: 16432 Comm: java Tainted: G OE 4.13.0-32-generic #35~16.04.1-Ubuntu Feb 18 17:34:47 kernel: Hardware name: Dell Inc. Precision 5520/0R6JFH, BIOS 1.3.3 05/08/2017 Feb 18 17:34:47 kernel: Call Trace: Feb 18 17:34:47 kernel: dump_stack+0x63/0x8b Feb 18 17:34:47 kernel: dump_header+0x97/0x225 Feb 18 17:34:47 kernel: ? mem_cgroup_scan_tasks+0xc4/0xf0 Feb 18 17:34:47 kernel: oom_kill_process+0x219/0x420 Feb 18 17:34:47 kernel: out_of_memory+0x11d/0x4b0 Feb 18 17:34:47 kernel: mem_cgroup_out_of_memory+0x4b/0x80 Feb 18 17:34:47 kernel: mem_cgroup_oom_synchronize+0x325/0x340 Feb 18 17:34:47 kernel: ? get_mem_cgroup_from_mm+0xa0/0xa0 Feb 18 17:34:47 kernel: pagefault_out_of_memory+0x36/0x7b Feb 18 17:34:47 kernel: mm_fault_error+0x8f/0x190 Feb 18 17:34:47 kernel: ? handle_mm_fault+0xcc/0x1c0 Feb 18 17:34:47 kernel: __do_page_fault+0x4c3/0x4f0 Feb 18 17:34:47 kernel: do_page_fault+0x22/0x30 Feb 18 17:34:47 kernel: ? page_fault+0x36/0x60 Feb 18 17:34:47 kernel: page_fault+0x4c/0x60 Feb 18 17:34:47 kernel: RIP: 0033:0x7fdeafb0fe2f Feb 18 17:34:47 kernel: RSP: 002b:00007fdeb0e1db80 EFLAGS: 00010206 Feb 18 17:34:47 kernel: RAX: 000000000001dff0 RBX: 00007fdea802d490 RCX: 00007fdeac17b010 Feb 18 17:34:47 kernel: RDX: 0000000000003bff RSI: 0000000000075368 RDI: 00007fdeac17b010 Feb 18 17:34:47 kernel: RBP: 00007fdeb0e1dc20 R08: 0000000000000000 R09: 0000000000000000 Feb 18 17:34:47 kernel: R10: 0000000000000022 R11: 0000000000000246 R12: 0000000000000000 Feb 18 17:34:47 kernel: R13: 00007fdeb0e1db90 R14: 00007fdeafff851b R15: 0000000000075368 Feb 18 17:34:47 kernel: Task in /docker/35f18c48d432510c76e76f2e7a962e64a1372de1dc4abd830417263907bea6e0 killed as a result of limit of /docker/35f18c48d432510c76e76f2e7a962e64a137 Feb 18 17:34:47 kernel: memory: usage 5120kB, limit 5120kB, failcnt 69 Feb 18 17:34:47 kernel: memory+swap: usage 0kB, limit 9007199254740988kB, failcnt 0 Feb 18 17:34:47 kernel: kmem: usage 1560kB, limit 9007199254740988kB, failcnt 0 Feb 18 17:34:47 kernel: Memory cgroup stats for /docker/35f18c48d432510c76e76f2e7a962e64a1372de1dc4abd830417263907bea6e0: cache:176KB rss:3384KB rss_huge:0KB shmem:144KB mapped_fil Feb 18 17:34:47 kernel: [ pid ] uid tgid total_vm rss nr_ptes nr_pmds swapents oom_score_adj name Feb 18 17:34:47 kernel: [16360] 0 16360 1073 178 8 3 0 0 sh Feb 18 17:34:47 kernel: [16426] 0 16426 609544 3160 47 4 0 0 java Feb 18 17:34:47 kernel: Memory cgroup out of memory: Kill process 16426 (java) score 2508 or sacrifice child Feb 18 17:34:47 kernel: Killed process 16426 (java) total-vm:2438176kB, anon-rss:3200kB, file-rss:9440kB, shmem-rss:0kB ...
Убийца OOM ядра убил приложение, потому что оно нарушило ограничение памяти cgroup
. Из приведенных выше журналов: память: использование 5120 КБ, ограничение 5120 Кб, ошибка 69
показывает, что она достигла предела, Убитый процесс 16426 (java) всего - виртуальная машина: 2438176 Кб, анон-rss: 3200 Кб, файл-rss:9440 Кб, shmem-rss:0 КБ
показывает, что он решил убить процесс 16426 это был наш java-процесс. В журналах содержится гораздо больше информации, которая может помочь определить причину, по которой убийца OOM убил ваш процесс, однако в нашем случае мы знаем, почему – мы нарушили ограничение памяти контейнера.
При проблеме с кучей, если мы столкнемся с ошибкой нехватки памяти с Пространством кучи Java
в качестве причины, мы сразу узнаем, что причиной является куча, и мы либо выделяем слишком много, либо нам нужно увеличить кучу (на самом деле определение основной причины этого перераспределения в коде – еще одна проблема…). Когда убийца OOM убивает ваш процесс, это не так просто – это могут быть прямые буферы, неограниченные области памяти без кучи ( Метапространство , кэш кода и т. Д.) Или даже другой процесс в контейнере. При расследовании необходимо учитывать довольно многое. На этой ноте я закончу этот пост.
В JVM, докере и оом-киллере можно было бы сказать гораздо больше о памяти кучи/не кучи, но я хочу, чтобы этот начальный пост был коротким, он просто должен быть базовым введением в использование памяти JVM. Надеюсь, если вы что-то и убрали из этого поста, так это то, что при использовании JVM нужно думать не только о куче, особенно в контейнерах с привязкой к памяти.
Смотрите Подробности NMT . ↩
Этот мне нужно изучить более подробно, так как я не смог найти о нем достоверной информации. ↩
Кусок арены, похоже, связан с аренами malloc, обязательно изучит это подробно. ↩
1 байт. Зачем использовать MiB? Потому что MB неоднозначен и может означать 1000 КБ или 1024 КБ, тогда как MiB всегда равен 1024 КБ. ↩
Смотрите этот отличный ответ для описания RSS. ↩
Подробное описание их можно найти здесь . ↩
Документация
docker
по этому вопросу превосходна – см. ограничения ресурсов . ↩См. подкачка памяти docker . ↩
Оригинал: “https://dev.to/wayofthepie/jvm-basic-memory-overview-535m”