Первоначально это было опубликовано в моем собственном блоге несколько лет назад, но я предпочел бы продолжить серию здесь, так что, перепост.
О чем этот пост?
Цель этого сообщения – дать обзор областей памяти кучи и не кучи 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 {
Vector v = 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”