Рубрики
Без рубрики

Внутренние компоненты JVM: Обзор памяти

Вступление Первоначально это было опубликовано в моем собственном блоге несколько лет назад, но я бы предпочел соавторство… С тегами jvm, java, docker.

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

О чем этот пост?

Цель этого сообщения – дать обзор областей памяти кучи и не кучи 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 нужно думать не только о куче, особенно в контейнерах с привязкой к памяти.

  1. Смотрите Подробности NMT . ↩

  2. Этот мне нужно изучить более подробно, так как я не смог найти о нем достоверной информации. ↩

  3. Кусок арены, похоже, связан с аренами malloc, обязательно изучит это подробно. ↩

  4. 1 байт. Зачем использовать MiB? Потому что MB неоднозначен и может означать 1000 КБ или 1024 КБ, тогда как MiB всегда равен 1024 КБ. ↩

  5. Смотрите этот отличный ответ для описания RSS. ↩

  6. Подробное описание их можно найти здесь . ↩

  7. Документация docker по этому вопросу превосходна – см. ограничения ресурсов . ↩

  8. См. подкачка памяти docker . ↩

Оригинал: “https://dev.to/wayofthepie/jvm-basic-memory-overview-535m”