1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/yu120-lemon-guide

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
JVM.md 170 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
gitlife-traslator Отправлено 30.11.2024 18:34 ec8a065

Class файл, константный пул

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

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

Характеристики константного пула:

  1. Динамичность: константный пул может изменяться во время выполнения программы. Это позволяет добавлять или удалять константы из пула без необходимости перекомпиляции всего класса.
  2. Строковые константы: в Java константные пулы содержат ссылки на строковые объекты. Если две строки имеют одинаковое содержимое, они будут ссылаться на один и тот же объект в константном пуле.
  3. Оптимизация: использование констант вместо литералов может ускорить выполнение программы за счёт уменьшения количества операций сравнения и преобразования типов.
  4. Безопасность: константы, определённые в одном классе, не могут быть изменены другим классом. Это обеспечивает безопасность данных и предотвращает несанкционированный доступ к информации.

Пул времени выполнения

Пул времени выполнения (runtime constant pool) — это часть структуры данных виртуальной машины Java (JVM), которая используется для хранения информации о классе во время его выполнения. Он является частью области метода (method area) в памяти JVM.

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

Информация в пуле времени выполнения используется различными компонентами JVM, такими как загрузчик классов, верификатор байт-кода и интерпретатор/компилятор Just-In-Time (JIT).

Глобальный пул строк

Строковый пул (string pool) — это структура данных в виртуальной машине Java, которая хранит строковые значения. Когда строка создаётся с помощью оператора new или строкового литерала, она помещается в пул, если её там ещё нет. Если строка уже существует в пуле, возвращается ссылка на существующую строку.

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

Константный пул для примитивных типов

Большинство классов-обёрток для примитивных типов в Java реализуют технологию константных пулов. Эти классы включают Byte, Short, Integer, Long, Character, Boolean, а также два типа с плавающей точкой.

Однако стоит отметить, что только те экземпляры этих классов, значения которых меньше или равны 127, могут использовать объекты из пула. Для значений больше 127 объекты создаются индивидуально.

Модель памяти JVM

JVM определяет унифицированную модель памяти, которая абстрагирует различия между аппаратными платформами и операционными системами. Модель памяти разделяет память на рабочую (стек) и основную (кучу). Поток не может напрямую обращаться к основной памяти, вместо этого он должен использовать рабочую память для обмена данными с другими потоками.

Рабочая память включает в себя:

— программный счётчик (program counter register, PC), который указывает на текущую инструкцию потока; — стек виртуальной машины (Java virtual machine stack, JVM stack), содержащий фреймы стека для каждого вызова метода; — собственный стек методов (native method stack), используемый для вызовов собственных методов.

Основная память включает:

— область методов (method area), где хранятся структуры данных, необходимые для выполнения методов; — кучу (heap), где размещаются объекты и массивы.

Каждый поток имеет свой собственный программный счётчик, стек виртуальной машины и собственный стек методов. Область методов и куча являются общими для всех потоков.

Программный счётчик

Программный счётчик — это регистр, который содержит адрес текущей инструкции потока. При выполнении каждой инструкции программный счётчик увеличивается на размер инструкции. Если поток выполняет вызов метода, программный счётчик сохраняет адрес следующей инструкции после возврата из метода.

Программные счётчики являются частными для каждого потока, поэтому каждый поток может иметь свой собственный счётчик. Они не подвержены ошибкам нехватки памяти (OutOfMemoryError), так как их размер фиксирован.

Стек виртуальной машины

Стек виртуальной машины — это структура данных, используемая для управления выполнением методов. Каждый раз, когда вызывается метод, в стеке создаётся новый фрейм стека (stack frame). Фрейм содержит локальные переменные, параметры метода, адреса возврата и другую информацию, необходимую для выполнения метода.

Когда метод завершает своё выполнение, его фрейм удаляется из стека. Если глубина стека становится слишком большой, возникает ошибка StackOverflowError. Также возможна ошибка OutOfMemoryError, если стек не может быть расширен.

Размер стека можно настроить с помощью параметра -Xss.

Собственный стек методов

Собственный стек методов используется для вызовов собственных (native) методов, которые реализованы на другом языке программирования, например C или C++. Этот стек аналогичен стеку виртуальной машины, но предназначен для собственных методов.

Реализация собственного стека методов зависит от конкретной реализации JVM. В некоторых реализациях собственный стек методов объединён со стеком виртуальной машины.

Область методов

Область методов — это область основной памяти, где хранятся метаданные классов, такие как пул констант, статические переменные и код метода. Она также называется не-кучей (non-heap) или постоянной областью (permanent generation).

Если область методов не может удовлетворить требования к памяти, возникает ошибка OutOfMemoryError. Размер области методов можно настроить с параметрами -XX:PermSize и -XX:MaxPermSize.

Куча

Куча — это основная область памяти, где размещаются все объекты и массивы, созданные в программе. Куча разделена на несколько поколений, чтобы оптимизировать сбор мусора.

Новое поколение (young generation) состоит из трёх областей: Eden, Survivor и Tenured. Объекты сначала размещаются в Eden. Если они выживают после первого цикла сборки мусора, они перемещаются в Survivor. После нескольких циклов сборки мусора объекты переходят в Tenured поколение.

Старое поколение (old generation) содержит долгоживущие объекты, которые редко меняются. Сбор мусора в старом поколении происходит реже, но требует больше времени.

Если куча не может выделить достаточно памяти для нового объекта, возникает ошибка OutOfMemoryError. Размеры молодого и старого поколений можно настроить с помощью параметров -Xms, -Xmx, -XX:NewRatio, -XX:SurvivorRatio и других. MinorGC использует алгоритм копирования

  • Сначала объекты, которые выжили в областях Eden и SurvivorFrom, копируются в область SurvivorTo (если возраст объекта соответствует стандарту старости, то он копируется в область старого поколения), при этом возраст этих объектов увеличивается на 1 (если SurvivorTo не хватает места, объект помещается в старое поколение).

  • Затем области Eden и SurvivorFrom очищаются от объектов.

  • Наконец, SurvivorTo и SurvivorFrom меняются местами, и область, которая раньше была SurvivorTo, становится областью SurvivorFrom для следующего GC.

Почему нельзя, чтобы областей Survivor было 0?

Если областей Survivor нет, это означает, что в новом поколении есть только одна область Eden. После каждого цикла сбора мусора все выжившие объекты переходят в старое поколение. Таким образом, пространство памяти старого поколения быстро заполняется, что приводит к запуску полного сборщика мусора (Full GC), который является самым медленным. Очевидно, что такой сборщик мусора неэффективен.

Почему нельзя, чтобы область Survivor была 1?

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

Однако если мы установим соотношение размеров областей памяти как 8:2, это может показаться «хорошим» решением. Предположим, что размер нового поколения составляет 100 МБ (область Survivor имеет размер 20 МБ). После сбора мусора остаётся 70 МБ активных объектов, которые переходят в область Survivor. В этот момент доступно только 5 МБ пространства в новом поколении. Вскоре после этого снова потребуется выполнить сбор мусора. Основная проблема такого сборщика мусора заключается в том, что ему приходится часто выполнять сбор мусора.

Почему областей Survivor две?

Если у нас есть две области Survivor, мы можем установить соотношение размеров областей Eden, From Survivor и To Survivor как 8:1:1. В этом случае коэффициент использования пространства нового поколения всегда составляет около 90%. Это соответствует ожидаемому уровню использования пространства. Кроме того, большинство объектов виртуальной машины имеют характеристики «рождения и смерти», поэтому новые объекты обычно создаются в области Eden с большим пространством. После сбора мусора выжившие объекты перемещаются в область Survivor. Если объект выживает в области Survivor, его возраст увеличивается на 1. Когда возраст достигает 15 (можно настроить с помощью -XX:+MaxTenuringThreshold), объект перемещается в старое поколение.

Заключение

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

Старое поколение (Old Generation)

В старом поколении хранятся объекты с длительным жизненным циклом. Объекты в старом поколении относительно стабильны, поэтому MajorGC не выполняется часто. Обычно перед выполнением MajorGC сначала выполняется MinorGC, чтобы переместить объекты из нового поколения в старое. MajorGC запускается, когда невозможно выделить достаточно большое непрерывное пространство для новых крупных объектов.

Процесс MajorGC

MajorGC использует алгоритм маркировки и очистки. Сначала выполняется сканирование всего старого поколения и маркировка выживших объектов. Затем выполняется очистка объектов, не помеченных как выжившие. Процесс MajorGC занимает много времени, так как включает в себя сканирование и очистку. MajorGC может привести к фрагментации памяти. Чтобы уменьшить потери памяти, обычно требуется объединение или маркировка объектов для облегчения последующего распределения. Когда старое поколение также заполняется и не может вместить новые объекты, возникает исключение OOM (Out of Memory).

Постоянное поколение (Perm Generation)

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

Java 8 и метаданные

В Java 8 постоянное поколение было удалено и заменено областью, называемой «пространство метаданных» (или «метапространство»). Пространство метаданных похоже на постоянное поколение, но оно не находится в виртуальной машине, а использует локальную память. По умолчанию размер пространства метаданных ограничен только доступной локальной памятью. Классы и их метаданные размещаются в локальной памяти, а строки и статические переменные классов — в куче Java. Это позволяет загружать любое количество классов без ограничения MaxPermSize, которое теперь контролируется доступным пространством локальной памяти.

Стратегия распределения памяти

Объекты обычно распределяются следующим образом:

  • Объекты предпочтительно размещаются в области Eden.
  • Крупные объекты сразу попадают в старое поколение.
  • Долгоживущие объекты также попадают в старое поколение. | Параметр | Описание | | :------------------------------ | ------------------------------------------------------------ | | -Xms | Начальный размер кучи. Например: -Xms256m | | -Xmx | Максимальный размер кучи. Например: -Xmx512m | | -Xmn | Размер нового поколения. Обычно составляет ⅓ или ¼ от Xmx. Новое поколение = Eden + 2 области Survivor. Фактически доступно пространство = Eden + 1 область Survivor, т. е. 90% | | -Xss | Для каждого потока JDK1.5+ размер стека составляет 1M. Как правило, если стек не слишком глубокий, 1M вполне достаточно | | -XX:NewRatio | Соотношение между новым поколением и старым поколением. Например, -XX:NewRatio=2 означает, что новое поколение занимает ⅓ всего пространства кучи, а старое поколение — ⅔ | | -XX:SurvivorRatio | Соотношение между областью Eden и двумя областями Survivor в новом поколении. Значение по умолчанию — 8, т.е. область Eden занимает 8/10 пространства нового поколения, а каждая из двух областей Survivor — по 1/10 | | -XX:PermSize | Начальный размер постоянного поколения | | -XX:MaxPermSize | Максимальный размер постоянного поколения | | -XX:+PrintGCDetails | Выводит информацию о GC | | -XX:+HeapDumpOnOutOfMemoryError | Заставляет виртуальную машину создавать дамп кучи при возникновении ошибки нехватки памяти, чтобы можно было проанализировать её |

Основная стратегия параметров

Размер каждой области влияет на производительность GC. Анализ активности данных является хорошим способом определить оптимальное распределение.

Активность данных: относится к размеру пространства, занимаемого долгоживущими объектами в куче после полного GC, в старом поколении.

Можно получить активность данных из журнала GC после полного GC в старом поколении, но более точный метод — это получение данных GC несколько раз после стабилизации программы и вычисление среднего значения. Отношение активности данных к размерам различных областей следующее:

Область Коэффициент
Общий размер 3–4 активности данных
Новое поколение 1–1,5 активности данных
Старое поколение 2–3 активности данных
Постоянное поколение 1,2–1,5 размера активности данных после полного GC

Например, если активность данных в старом поколении составляет 300 Мбайт, размеры различных областей могут быть следующими:

Общий размер: 1200 МБ = 300 МБ × 4

Новое поколение: 450 МБ = 300 МБ × 1,5

Старое поколение: 750 МБ = 1200 МБ − 450 МБ

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

Уровень ссылок

В Java существует четыре уровня ссылок от сильного до слабого: сильная ссылка → слабая ссылка → мягкая ссылка → фантомная ссылка.

Когда сборщик мусора выполняет свою работу, некоторые объекты будут собраны, а другие — нет. Сборщик мусора начинает с корневого объекта Object, чтобы пометить живые объекты, а затем собирает недоступные и некоторые ссылочные объекты. java.lang.OutOfMemoryError: GC overhead limit exceeded

Ошибка «java.lang.OutOfMemoryError:GC overhead limit exceeded» означает, что приложение исчерпало всю доступную память и сборщик мусора (Garbage Collector, GC) не может её освободить.

Эта проблема похожа на ошибку «Java heap space», и для её решения можно обратиться к предыдущей информации.

Сценарий 3: Permgen space

Данная ошибка указывает на то, что пространство Permanent Generation (Permgen), используемое для хранения объектов, полностью занято. Это обычно происходит из-за большого количества или размера загружаемых классов.

Анализ причин:

В пространстве Permgen хранятся следующие объекты:

— определения классов, включая имена классов, поля, методы и байт-код;

— константы;

— массивы объектов и типы массивов, связанные с классами;

— информация о классах после JIT-компиляции.

Объём использования пространства Permgen напрямую связан с количеством и размером загруженных классов.

Решения:

Выбор решения зависит от момента возникновения ошибки Permgen space:

  1. Если ошибка возникает при запуске приложения, можно увеличить размер пространства Permgen, изменив параметр -XX:MaxPermSize.

  2. Если ошибка появляется при повторном развёртывании приложения, возможно, проблема связана с тем, что классы не были перезагружены. В этом случае достаточно перезапустить JVM.

  3. Если ошибка возникает во время работы приложения, вероятно, оно динамически создаёт большое количество классов с коротким жизненным циклом. По умолчанию JVM не выгружает такие классы. Можно настроить параметры -XX:+CMSClassUnloadingEnabled и -XX:+UseConcMarkSweepGC, чтобы разрешить выгрузку классов.

Если эти методы не помогают, можно использовать команду jmap для создания дампа памяти (jmap -dump:format=b,file=dump.hprof ), а затем проанализировать наиболее ресурсоёмкие загрузчики классов и повторяющиеся классы с помощью инструмента Eclipse MAT.

Сценарий 4: Metaspace

JDK 1.8 использует Metaspace вместо Permanent Generation. Ошибка «Metaspace is full» указывает на то, что Metaspace заполнено из-за множества загруженных классов или их большого размера.

Причины и решения этой проблемы аналогичны сценарию 3, но следует обратить внимание на настройку параметра -XX:MaxMetaspaceSize для управления размером пространства Metaspace.

Сценарий 5: Unable to create new native thread

Каждый поток в Java требует определённого объёма памяти. При попытке JVM создать новый собственный поток (native thread) и нехватке ресурсов возникает ошибка «Unable to create new native thread».

Причины:

— Количество потоков превышает ограничение ulimit для максимального числа потоков в операционной системе.

— Число потоков превышает значение kernel.pid_max, которое можно изменить только путём перезагрузки.

— Недостаточно собственной памяти.

Обычно процесс протекает следующим образом:

  1. Приложение внутри JVM запрашивает создание нового потока Java.

  2. Собственный метод JVM проксирует этот запрос и пытается создать собственный поток через операционную систему.

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

  4. Если виртуальная память операционной системы исчерпана или есть ограничения из-за 32-битного процесса, операционная система отклоняет запрос на выделение памяти.

  5. JVM выдаёт ошибку java.lang.OutOfMemoryError:Unable to create new native thread.

Решение:

Возможные действия включают обновление конфигурации, уменьшение размера кучи Java, исправление утечек памяти в приложении, ограничение размера пула потоков, уменьшение размера стека потоков с помощью параметра -Xss и увеличение максимального количества потоков на уровне операционной системы с помощью команд ulimia-a и ulimit-u xxx.

Сценарий 6: Out of swap space?

Ошибка «Out of swap space?» указывает на исчерпание всей доступной виртуальной памяти. Виртуальная память состоит из физической памяти и пространства подкачки (swap space).

Причины:

— Недостаток адресного пространства.

— Исчерпание физической памяти.

— Утечка собственной памяти в приложении (например, постоянное выделение и отсутствие освобождения памяти).

Для проверки можно выполнить команду jmap -histo:live , которая принудительно выполнит полный сборщик мусора. Если после нескольких попыток объём памяти значительно уменьшается, это указывает на проблему с Direct ByteBuffer.

Решение:

Действия зависят от причины ошибки:

— Переход на 64-битную архитектуру для увеличения адресного пространства.

— Проверка на наличие проблем с Inflater/Deflater с помощью Arthas и явный вызов метода end при необходимости.

— Настройка параметра -XX:MaxDirectMemorySize для уменьшения порога для проблем с Direct ByteBuffer.

— Обновление сервера или изоляция развёртывания для предотвращения конкуренции за ресурсы.

Сценарий 7: Kill process or sacrifice child

Существует ядро Linux, называемое Out of Memory Killer (OOM Killer), которое в условиях низкой доступной памяти «убивает» некоторые процессы. OOM Killer оценивает все процессы и выбирает те, которые имеют низкий рейтинг, для «убийства» с целью освобождения памяти. Правила оценки описаны в статье Surviving the Linux OOM Killer.

В отличие от других ошибок OOM, ошибка «Kill process or sacrifice child» не инициируется JVM, а является результатом работы ядра операционной системы.

Причина:

По умолчанию Linux позволяет процессам запрашивать больше памяти, чем доступно в системе, используя механизм «перегрузки». Однако это также может привести к риску «переполнения». Например, некоторые процессы могут постоянно занимать системную память, что приводит к нехватке памяти для других процессов. В таких случаях система автоматически активирует OOM Killer для поиска и завершения низкоприоритетных процессов, освобождая таким образом ресурсы памяти.

Решение:

Варианты действий включают обновление сервера, изоляцию развёртывания и оптимизацию OOM Killer.

Сценарий 8: Requested array size exceeds VM limit

Виртуальная машина Java (VM) ограничивает максимальный размер массива. Ошибка указывает на попытку создания массива, превышающего допустимый размер. Обычно максимальное значение составляет Integer.MAX_VALUE-2 перед проверкой доступности данных в системе.

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

Сценарий 9: Direct buffer memory

Java позволяет приложениям напрямую обращаться к внешней памяти через Direct ByteBuffer, что используется в высокопроизводительных программах для быстрого ввода-вывода с использованием Memory Mapped File.

Причина:

Размер Direct ByteBuffer по умолчанию составляет 64 МБ. Превышение этого предела вызывает ошибку «Direct buffer memory».

Решение:

Можно предпринять следующие шаги:

— Использовать метод ByteBuffer.allocateDirect для работы с Direct ByteBuffer в Java. Проверить код на прямое или косвенное использование NIO, например, netty или jetty.

— Настроить верхний предел Direct ByteBuffer с помощью параметра -XX:MaxDirectMemorySize.

— Проверить наличие параметра -XX:+DisableExplicitGC, который может сделать System.gc() неэффективным.

— Найти утечки памяти при использовании внешней памяти и попытаться освободить занятое Direct ByteBuffer пространство с помощью отражения и вызова метода clean() объекта sun.misc.Cleaner.

— Увеличить конфигурацию, если действительно не хватает памяти. Цитата счётчика

Цитата счётчика (Reference Count) добавляет в объект счётчик цитат. Каждый раз, когда объект цитируется, значение счётчика увеличивается на 1. Когда цитата становится недействительной, значение счётчика уменьшается на 1. Если значение счётчика равно 0, объект не может быть использован и может считаться мусором.

Достижимость анализа

Алгоритм достижимости начинается с корней (Roots) и исследует все объекты, которые могут быть достигнуты из этих корней. Корни включают:

  • переменные стека потоков;
  • статические переменные;
  • константы пула;
  • JNI-вызовы.

Если объект недоступен из корней, он считается мусором и может быть удалён сборщиком мусора (Garbage Collector).

Сбор мусора

Сбор мусора включает два основных этапа: маркировку (Mark) и очистку (Sweep). На этапе маркировки сборщик мусора находит все активные объекты. Затем на этапе очистки удаляются все немаркированные объекты.

Существуют также другие алгоритмы сбора мусора, такие как копирование (Copying), маркировка-очистка (Mark-Sweep) и маркировка-компактирование (Mark-Compact). Алгоритм копирования копирует все живые объекты в новое пространство памяти, освобождая старое пространство для повторного использования. Алгоритмы маркировки-очистки и маркировки-компактирования также используют маркировку для определения живых объектов, но затем выполняют очистку или компактирование соответственно.

В Java Virtual Machine (JVM) используются различные алгоритмы сбора мусора в зависимости от поколения объектов. Молодые объекты (Young Generation) обычно используют алгоритм копирования, а старые объекты (Old Generation) — алгоритмы маркировки-очистки или маркировки-компактирования. Эден: разделение и сбор

Когда Эден заполняется, запускается Minor GC молодого поколения. Процесс выглядит следующим образом:

  1. После первого GC в Эдене выжившие объекты перемещаются в один из Survivor разделов (далее from).
  2. В Эдене снова выполняется GC с использованием алгоритма копирования. Выжившие объекты копируются в to раздел, после чего from можно очистить.

В этом процессе один из Survivor разделов всегда остаётся пустым. По умолчанию соотношение Eden, from и to составляет 8:1:1, что приводит к 10% потере пространства. Это соотношение настраивается параметром -XX:SurvivorRatio (по умолчанию 8).

Старое поколение (Old/Tenured Generation)

Для старого поколения обычно используются алгоритмы «маркировка-очистка» и «маркировка-сборка», поскольку выживаемость объектов в старом поколении высока, а пространство велико. Объекты попадают в старое поколение следующим образом:

  • Продвижение: если объект достаточно старый, он продвигается в старое поколение.
  • Гарантия распределения: если выживших объектов после сборки мусора в молодом поколении больше 10%, и Survivor не может вместить их все, они распределяются в старом поколении.
  • Большие объекты сразу распределяются в старом поколении: объекты, превышающие определённый размер, сразу распределяются в старом поколении.
  • Динамическое определение возраста объекта: некоторые алгоритмы сборки мусора не требуют достижения объектом возраста 15 для продвижения в старое поколение, используя динамические методы расчёта. Например, если сумма размеров объектов одинакового возраста в Survivor превышает половину размера Survivor или равна или больше возраста, эти объекты переходят в старое поколение.

Разделение на разделы

GC-сборщик мусора имеет различные конфигурации для молодого и старого поколений. Вот некоторые из них:

  • -XX:+UseSerialGC: последовательный сборщик для молодого и старого поколений.
  • -XX:+UseParNewGC: ParNew для молодого поколения и Serial Old для старого.
  • -XX:+UseParallelGC: ParallelGC для молодого поколения и Serial Old для старого.
  • -XX:+UseParallelOldGC: параллельный сборщик как для молодого, так и для старого поколений.
  • -XX:+UseConcMarkSweepGC: ParNew для молодого поколения и CMS для старого.
  • -XX:+UseG1GC: сборщик G1.
  • -XX:+UseZGC: сборщик ZGC.

Сборщик молодого поколения

Существует несколько типов сборщиков для молодого поколения:

  • Serial: обрабатывает сборку мусора только одним потоком, останавливая все пользовательские потоки во время процесса. Самый простой сборщик, но он всё ещё полезен. Он используется в клиентских приложениях, где создание большого количества объектов не происходит часто, и пользователи не замечают значительных пауз. Этот сборщик потребляет меньше ресурсов и является более лёгким.

  • ParNew: многопоточная версия Serial. Несколько потоков GC выполняют сборку мусора параллельно. Процесс по-прежнему требует остановки пользовательских потоков. ParNew стремится сократить время паузы, предлагая улучшенную производительность по сравнению с Serial в многопроцессорных средах. Однако переключение между потоками требует дополнительных затрат, поэтому в однопроцессорной среде ParNew может уступать Serial.

  • Parallel Scavenge: ещё одна многопоточная версия сборщика мусора. Основное отличие от ParNew заключается в том, что Parallel Scavenge ориентирован на пропускную способность процессора и способен быстро завершать задачи, что подходит для фоновых вычислений без взаимодействия с пользователем.

Сборщики старого поколения

  • Serial Old: соответствует Serial для молодого поколения, также использует алгоритм копирования.
  • Parallel Old: версия Parallel Scavenge для старого поколения, ориентированная на пропускную способность.
  • CMS (Concurrent Mark Sweep): сборщик мусора, стремящийся минимизировать паузы. Использует несколько потоков для сканирования памяти старого поколения и маркировки объектов для удаления. Пауза возникает при маркировке ссылок на объекты в старом поколении или изменении памяти во время сборки мусора. CMS использует больше процессорных ресурсов для обеспечения более высокой пропускной способности. Если доступно больше процессоров, CMS предпочтительнее параллельного сборщика. Параметр -XX:+UseParNewGC включает использование CMS.

Процесс работы CMS включает начальную маркировку (только прямые ссылки на GC Roots), параллельную маркировку (поиск всех достижимых объектов) и повторную маркировку (исправление изменений, вызванных работой приложения во время параллельной маркировки). Затем следует параллельная очистка.

CMS запускается, когда использование старого поколения достигает 80%. Параметры -XX:CMSInitiatingOccupancyFraction=80 и -XX:+UseCMSInitiatingOccupancyOnly управляют этим процессом.

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

  • G1 (Garbage First): сборщик мусора для серверных приложений, заменяющий CMS. G1 использует локальный подход и основан на разделении памяти на регионы. По умолчанию G1 становится сборщиком мусора в серверном режиме, начиная с JDK 9, заменяя Parallel Scavenge и Parallel Old. Алгоритм реализации сборщика мусора (G1)

Сборщик мусора G1 реализует алгоритм, основанный на маркировке и копировании. Сборщик мусора работает в четыре этапа:

  1. Начальная маркировка: только помечает объекты, которые могут быть напрямую связаны с корнями сборщика мусора GC Roots, и изменяет указатель TAMS. Этот этап требует остановки потоков, но он очень короткий и синхронизируется с Minor GC, поэтому сборщик мусора не имеет дополнительной паузы на этом этапе.

  2. Параллельная маркировка: начинается с корней сборщика мусора и анализирует достижимость объектов в куче. Рекурсивно сканирует всю диаграмму объектов кучи, находит объекты для сбора и обрабатывает изменения ссылок между этапами. Этот этап может выполняться параллельно с пользовательскими потоками.

  3. Окончательная маркировка: короткая пауза для пользовательских потоков после завершения параллельной маркировки. Обрабатывает изменения ссылок, оставшиеся после параллельной фазы.

  4. Фаза очистки: обновляет статистические данные о регионах, сортирует регионы по стоимости и затратам на сбор, планирует сбор на основе ожидаемого времени паузы пользователя и выбирает несколько регионов для сбора. Копирует живые объекты из выбранных регионов в пустые регионы и очищает все пространство старого региона. Эта операция включает перемещение живых объектов и требует паузы для пользовательских потоков. Она выполняется несколькими потоками сборщика мусора параллельно.

В памяти кучи сборщика мусора G1 разделена на несколько равных блоков памяти, называемых регионами. Каждый регион представляет собой логически непрерывный блок памяти.

Регион

Размер каждого региона можно указать с помощью параметра -XX:G1HeapRegionSize. Размер должен быть степенью двойки: 1M, 2M, 4M, 8M или 16M. Если параметр не указан, размер региона рассчитывается при инициализации кучи как 2048 частей.

Режимы сборки мусора

  • Young GC: происходит в молодом поколении. Большинство объектов (кроме больших объектов) размещаются в регионе Eden. Когда регион Eden заполняется, запускается Young GC. Процесс аналогичен предыдущему Young GC и перемещает активные объекты в Survivor Region или продвигает их в Old Region. Освободившиеся регионы помещаются в список свободных регионов.

Параметры:

  • -XX:MaxGCPauseMillis: устанавливает целевое время для процесса сбора, по умолчанию 200 мс.
  • -XX:G1NewSizePercent: минимальный размер молодого поколения, по умолчанию 5%.
  • -XX:G1MaxNewSizePercent: максимальный размер молодого поколения, по умолчанию 60%.
  • Mixed GC: когда всё больше объектов продвигаются в старое поколение, чтобы избежать переполнения кучи, виртуальная машина запускает смешанный сборщик мусора. Mixed GC не является полным GC, а собирает часть старого поколения. Можно выбрать, какие регионы старого поколения собирать. Это позволяет контролировать время сбора мусора.

  • Full GC: если скорость выделения объектов слишком высока, смешанный GC не успевает собрать мусор, и старое поколение заполняется. В этом случае запускается полный сборщик мусора, который является однопоточным и последовательным. Full GC следует избегать, так как он вызывает длительные паузы.

Сборщик мусора G1 предназначен для больших куч памяти. Он разделяет кучу на разные области и выполняет сбор мусора в этих областях параллельно. После сбора мусора G1 немедленно объединяет свободные пространства, чтобы уменьшить фрагментацию. CMS выполняет объединение памяти во время полной остановки (STW). Для разных областей G1 определяет приоритеты на основе количества мусора. Параметр -XX:UseG1GCJVM используется для включения сборщика мусора G1.

Основные этапы работы:

  • Начальная маркировка: помечаются объекты, доступные из корней сборщика мусора. Происходит остановка потоков (STW).
  • Параллельная маркировка: помечаются производные объекты от доступных объектов, собирается информация о живых объектах в каждом регионе. Сборщик мусора и пользовательские потоки могут работать параллельно.
  • Окончательная маркировка: помечаются пропущенные или изменившие ссылки объекты во время параллельной маркировки. Происходит остановка потоков (STW).
  • Подсчёт живых данных и очистка: очищается регион, если в нём нет живых объектов. Регион добавляется в список свободных.

Параметр -XX:InitiatingHeapOccupancyPercent определяет, когда сборщик мусора будет запущен при достижении определённого процента заполнения старого поколения.

Преимущества:

  • Параллельность и параллелизм позволяют эффективно использовать многоядерные процессоры.
  • Разделение поколений позволяет работать без дополнительных сборщиков мусора.
  • Объединение памяти предотвращает фрагментацию, основанную на алгоритме «метка-очистка» для всего пространства и «копирование» для локальных областей.
  • Можно установить максимальное время паузы сбора мусора.

Недостатки:

  • Требуется запоминание набора для отслеживания связей между молодым и старым поколениями.
  • Занимает значительное количество памяти, возможно, до 20% или более от всей кучи.

Область действия: охватывает несколько поколений.

Сценарии использования: сборщик мусора G1 подходит для сценариев, где важно минимизировать время паузы.

Тип алгоритма: общий алгоритм «метка-очистка», локально использует алгоритм «копирования».

ZGC (Z Garbage Collector)

ZGC — это новый сборщик мусора с низким временем задержки, представленный в JDK 11. Он находится в экспериментальной стадии разработки и предлагает ещё более низкую задержку, чем Shenandoah. ZGC также превосходит G1 по пропускной способности и приближается к Parallel GC по производительности.

Как и ParNew и G1 в CMS, ZGC использует алгоритм «метка-копирование», но с существенными улучшениями: ZGC почти полностью параллелен на этапах маркировки, перемещения и перестановки, что делает его ключевым фактором в достижении времени паузы менее 10 мс. Этапы сбора мусора ZGC включают:

  • Начальную маркировку.
  • Повторную маркировку.
  • Первоначальное перемещение.

Из них начальная маркировка и первоначальное перемещение требуют только сканирования всех корней сборщика мусора, и время обработки зависит от размера корней. Повторная маркировка обычно занимает не более 1 мс, а если превышает 1 мс, то переходит в параллельную фазу маркировки. Таким образом, большинство пауз ZGC зависят от размера корней сборщика мусора, а не от размера кучи или активных объектов.

По сравнению с ZGC, G1 требует полного останова (STW) на этапе перемещения, и время паузы увеличивается с размером активных объектов.

Память ZGC организована аналогично G1 и Shenandoah, но отличается динамическими регионами с тремя размерами: маленькими (2 МБ), средними (32 МБ) и большими (динамический размер, кратный 2 МБ).

Чтобы включить ZGC в Java, используйте параметры -XX:+UnlockExperimentalVMOptions -XX:+UseZGC.

Shenandoah

Shenandoah похож на G1, включая организацию памяти на основе регионов и поддержку больших объектов (Humongous Region). Однако есть три ключевых отличия:

  • Shenandoah поддерживает параллельный алгоритм организации, в отличие от G1, где организация выполняется многопоточно, но не может работать параллельно с пользовательским кодом.
  • По умолчанию не использует разделение поколений.
  • Использует матрицу соединений (Connection Matrix) для отслеживания ссылок между регионами вместо запоминания набора (Remembered Set), что снижает потребление памяти и вычислительную сложность. Шенандоа имеет более высокую степень параллелизма, чем G1. Для этого требуется лишь кратковременное «Stop The World» на этапах начальной маркировки, окончательной маркировки, обновления начальных ссылок и обновления окончательных ссылок. На остальных этапах можно выполнять параллельное выполнение с пользовательской программой.

Основные детали параллельной маркировки, параллельного сбора и параллельного обновления ссылок:

  • Параллельная маркировка (Concurrent Marking);
  • Параллельный сбор (Concurrent Evacuation);
  • Параллельное обновление ссылок (Concurrent Update Reference).

Высокая степень параллельности Shenandoah позволяет достичь сверхнизкого времени простоя, но более высокая сложность также сопровождается более высокими системными издержками, что в определённой степени влияет на пропускную способность. Ниже представлена сравнительная диаграмма времени простоя и системных издержек Shenandoah и различных сборщиков мусора:

Сборщик мусора Время простоя Системные издержки
Shenandoah Сверхнизкое Высокие
G1 Среднее Средние
Parallel GC Высокое Низкие

OracleJDK не поддерживает Shenandoa. Если вы используете OpenJDK 12 или некоторые версии JDK с поддержкой Shenandoah, вы можете включить его с помощью следующих параметров: -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC.

Формат журнала GC

  • Параллельный журнал GC YoungGC:
    • [Изображение ParallelGCYoungGC журнала]
  • Параллельный журнал FullGC:
    • [Изображение ParallelGCFullGC журнала].

Лучшие практики

Для разных сборщиков мусора в JVM посмотрите, какие параметры установлены по умолчанию, и не доверяйте чужим советам. Пример команды:

java -XX:+PrintFlagsFinal -XX:+UseG1GC 2>&1 | grep UseAdaptiveSizePolicy

PrintCommandLineFlags позволяет просматривать текущий используемый сборщик мусора и некоторые значения по умолчанию.

# java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=127905216 -XX:MaxHeapSize=2046483456 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
openjdk version "1.8.0_41"
OpenJDK Runtime Environment (build 1.8.0_41-b04)
OpenJDK 64-Bit Server VM (build 25.40-b25, mixed mode)

Параметры G1 для сборщика мусора

# 1. Основные параметры
-server                  # Режим сервера
-Xmx12g                  # Начальная куча
-Xms12g                  # Максимальная куча
-Xss256k                 # Размер стека памяти для каждого потока
-XX:+UseG1GC             # Использовать сборщик мусора G1 (Garbage First)
-XX:MetaspaceSize=256m   # Начальный размер метапространства
-XX:MaxMetaspaceSize=1g  # Максимальный размер метапространства
-XX:MaxGCPauseMillis=200 # Максимальное время паузы для каждого YGC / MixedGC (ожидаемая максимальная пауза)

# 2. Обязательные параметры
-XX:+PrintGCDetails            # Вывести подробный журнал GC
-XX:+PrintGCDateStamps         # Вывести метку времени GC (в формате даты, например, 2013-05-04T21:53:59.234+0800)
-XX:+PrintTenuringDistribution # Вывести распределение объектов: для анализа ситуации с продвижением объектов и продвижения, приводящего к высокой паузе, посмотреть журнал распределения возраста объектов
-XX:+PrintHeapAtGC                 # Выводить информацию о куче до и после выполнения GC
-XX:+PrintReferenceGC              # Вывести информацию об обработке ссылок: сильные ссылки/слабые ссылки/мягкие ссылки/виртуальные ссылки/метод finalize
-XX:+PrintGCApplicationStoppedTime # Вывести время STW
-XX:+PrintGCApplicationConCurrentTime # Вывести продолжительность работы службы во время интервала GC

# 3. Параметры разделения журналов
-XX:+UseGCLogFileRotation   # Включить разделение файлов журнала
-XX:NumberOfGCLogFiles=14   # Разделить максимум несколько файлов, после чего начать запись с начала файла
-XX:GCLogFileSize=32M       # Ограничение размера каждого файла, превышение которого вызывает разделение
-Xloggc:/path/to/gc-%t.log  # Путь вывода файла журнала GC, использование%t в качестве имени файла журнала, например gc-2021-03-29_20-41-47.log

Параметры CMS для сборщика мусора

# 1. Основные параметры
-server   # Режим сервера
-Xmx4g    # Максимально допустимый объём кучи JVM, динамически распределяемый
-Xms4g    # Первоначальный объём кучи JVM, обычно такой же, как Xmx, чтобы избежать повторного выделения памяти после каждого GC
-Xmn256m  # Объём памяти молодого поколения
-Xss512k  # Установить размер стека памяти каждого потока
-XX:+DisableExplicitGC                # Игнорировать ручной вызов GC, вызов System.gc() становится пустым вызовом, полностью не вызывая GC
-XX:+UseConcMarkSweepGC               # Использовать сборщик мусора CMS
-XX:+CMSParallelRemarkEnabled         # Уменьшить паузу при маркировке
-XX:+UseCMSCompactAtFullCollection    # Сжатие старого поколения при полном GC
-XX:+UseFastAccessorMethods           # Быстрая оптимизация примитивных типов
-XX:+UseCMSInitiatingOccupancyOnly    # Использовать ручную настройку для запуска CMS-сборки
-XX:LargePageSizeInBytes=128m         # Размер страницы памяти
-XX:CMSInitiatingOccupancyFraction=70 # Начать сбор CMS после использования 70%

# 2. Обязательные параметры
-XX:+PrintGCDetails                # Вывести подробный журнал GC
-XX:+PrintGCDateStamps             # Вывести отметку времени GC (в формате даты, например, 2013-05-04T21:53:59.234+0800)
-XX:+PrintTenuringDistribution     # Вывести распределение объектов: проанализировать ситуацию с продвижением объектов и продвижением, приводящим к высокой паузе, просмотреть журнал распределения возраста объектов
-XX:+PrintHeapAtGC                 # Выводить информацию о куче до и после выполнения GC
-XX:+PrintReferenceGC              # Вывести информацию об обработке ссылок: сильные ссылки/слабые ссылки/мягкие ссылки/виртуальные ссылки/метод finalize
-XX:+PrintGCApplicationStoppedTime # Вывести время STW
-XX:+PrintGCApplicationConCurrentTime # Вывести продолжительность работы службы во время интервала GC

# 3. Параметры разделения журналов
-XX:+UseGCLogFileRotation   # Включить разделение файлов журнала
-XX:NumberOfGCLogFiles=14   # Разделить максимум несколько файлов, после чего начать запись с начала файла
-XX:GCLogFileSize=32M       # Ограничение размера каждого файла, превышение которого вызывает разделение
-Xloggc:/path/to/gc-%t.log  # Путь вывода файла журнала GC, использование%t в качестве имени файла журнала, например gc-2021-03-29_20-41-47.log

Использование CMS в тестовой и промежуточной среде JVM (jdk8)

-server -Xms256M -Xmx256M -Xss512k -Xmn96M -XX:MetaspaceSize=128M -XX:MaxMetaspaceSize=128M -XX:InitialHeapSize=256M -XX:MaxHeapSize=256M  -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=80 -XX:+CMSClassUnloadingEnabled -XX:+CMSParallelRemarkEnabled -XX:+CMSScavengeBeforeRemark
``` **Сцена 3: OOM в MetaSpace**

*Ситуация:*

После запуска JVM или в определённый момент времени размер используемого MetaSpace начинает постоянно увеличиваться, и каждый сборщик мусора (GC) не может освободить память. Увеличение размера MetaSpace также не решает проблему полностью.

*Причина:*

Прежде чем обсуждать, почему происходит OOM, давайте рассмотрим, какие данные хранятся в этом пространстве. В Java 7 и ранее строковые константы помещались в область PermGen. Все строки, которые были интернированы, существовали там. Поскольку String.intern не контролируется, значение параметра `-XX:MaxPermSize` трудно установить, и часто возникает исключение `java.lang.OutOfMemoryError: PermGen space`. Поэтому в Java 7 литералы, статические переменные классов и символьные ссылки были перемещены в кучу. А в Java 8 PermGen был удалён, и его заменило пространство MetaSpace. Из Сценария 1 мы знаем, что для предотвращения дополнительных затрат на сбор мусора из-за эластичного масштабирования мы установим значения параметров `-XX:MetaSpaceSize` и `-XX:MaxMetaSpaceSize` фиксированными. Однако это может привести к невозможности расширения пространства при нехватке памяти и частым запускам GC, что в конечном итоге приводит к OOM. Таким образом, основная причина заключается в том, что ClassLoader постоянно загружает новые классы в память, обычно это происходит при динамической загрузке классов.

*Стратегия:*

Можно выполнить дамп снимка и использовать JProfiler или MAT для анализа гистограммы классов или напрямую использовать команду для определения конкретного пакета, который увеличивает количество классов. Если невозможно определить причину с общей точки зрения, можно добавить параметры `-XX:+TraceClassLoading` и `-XX:+TraceClassUnLoading` для отслеживания подробной информации о загрузке и выгрузке классов.

**Сцена 4: Преждевременное продвижение**

*Ситуация:*

Эта ситуация в основном возникает в коллекторах поколений. Профессиональный термин — «преждевременное повышение» (Premature Promotion). 90% объектов создаются и уничтожаются быстро, и только после нескольких циклов сбора мусора в Young они продвигаются в Old. Каждый цикл сбора мусора увеличивает возраст объекта на 1, и это контролируется параметром `-XX:MaxTenuringThreshold`. Преждевременное повышение обычно не влияет напрямую на GC, но оно может сопровождаться плавающим мусором, неудачей большого объекта и другими проблемами. Однако эти проблемы не возникают сразу, и мы можем наблюдать следующие явления, чтобы определить, произошло ли преждевременное повышение:

— Скорость распределения близка к скорости продвижения, а возраст объектов невелик.
В журнале GC появляется информация, такая как «Desired survivor size 107347968 bytes, new threshold 1(max 6)», что указывает на то, что объекты будут продвигаться в Old после одного цикла GC.
— Full GC происходит довольно часто, и после одного цикла GC изменение в Old значительно.
Например, если порог сбора в Old составляет 80%, он снижается до 10% после одного цикла GC, это означает, что 70% времени жизни объектов в Old на самом деле очень короткое.

Последствия преждевременного повышения:
— Young GC происходит часто, общая пропускная способность снижается.
— Full GC происходит часто, могут быть значительные паузы.

*Причины:*

Основные причины включают:
— Область Young/Eden слишком мала: если она слишком мала, время, необходимое для заполнения Eden, будет коротким, и объекты, которые должны быть собраны, будут участвовать в GC и продвинуты. Young GC использует алгоритм копирования, и копирование занимает больше времени, чем маркировка. Это означает, что время Young GC в основном связано со временем копирования (CMS сканирует Card Table или G1 сканирует Remember Set, что является другой проблемой). Невозможность своевременно собрать объекты увеличивает стоимость сбора, поэтому Young GC занимает больше времени и не может быстро освободить место, что приводит к увеличению количества Young GC.
— Скорость выделения слишком высока: можно наблюдать изменения в скорости выделения Mutator до и после возникновения проблем. Если есть заметные колебания, можно попытаться проанализировать сетевой трафик, медленные запросы к хранилищу и другие данные, чтобы увидеть, загружается ли большое количество данных в память.

*Стратегии:*

Если область Young/Eden слишком мала, мы можем увеличить область Young без изменения общего объёма кучи. Обычно область Old должна быть в 2–3 раза больше активной области, и лучше всего оставить 3 раза для учёта плавающего мусора. Остальное можно выделить Young.

Для оптимизации преждевременного продвижения исходная конфигурация была Young 1.2G + Old 2.8G. После наблюдения за CMS GC было обнаружено, что выжившие объекты составляют около 300–400M, поэтому размер Old был установлен примерно на 1.5G, оставив 2.5G для Young. Только параметр Young region был изменён (-Xmn), и общее время GC сократилось с 1100 мс до 500 мс, а время одного Young GC уменьшилось с 26 раз до 11 раз. Общее время GC не увеличилось, и частота CMS GC снизилась с примерно 40 минут до примерно 7 часов 30 минут.

Если скорость выделения слишком высокая:
— Если это спорадическое увеличение, мы можем найти проблему с помощью инструмента анализа памяти и оптимизировать код с точки зрения бизнеса.
— Если оно постоянное, текущий сборщик больше не соответствует требованиям Mutator. В этом случае мы либо увеличиваем объём виртуальной машины Mutator, либо настраиваем сборщик GC или увеличиваем пространство.

Заключение:
Преждевременное продвижение обычно не проявляется явно, но может вызвать проблемы с ухудшением работы сборщика мусора после длительного накопления. Поэтому лучше предотвратить это заранее. Мы можем посмотреть, есть ли такие явления в нашей системе, и попробовать оптимизировать их, если это применимо. Оптимизация одной строки кода имеет высокую рентабельность инвестиций. Если мы наблюдаем за областью Old до и после, и обнаруживаем, что можно собрать лишь небольшую часть, например, от 80% до 60%, это означает, что большинство наших объектов выживают, и пространство Old можно разумно увеличить.

**Сцена 5: Частый полный сборщик CMS**

*Ситуация:*

Полный сборщик мусора CMS происходит часто, но каждый раз затрачиваемое время не слишком велико, и общее максимальное STW находится в приемлемом диапазоне. Однако из-за частых GC пропускная способность значительно снижается.

*Причины:*

Это распространённая ситуация, которая обычно связана с тем, что один поток, отвечающий за обработку полного сборщика CMS, непрерывно опрашивает после завершения Young GC, используя метод shouldConcurrentCollect() для проверки того, выполнены ли условия сбора. Если условия соблюдены, он запускает фоновый сбор с использованием collect_in_background(). Опрос выполняется с использованием метода sleepBeforeNextCycle(), а интервал определяется параметром -XX:CMSWaitDuration, по умолчанию равным 2 секундам.

*Стратегии:*

Обработка этой распространённой проблемы утечки памяти в основном следует одному и тому же процессу, основные шаги которого включают:

Анализ Histogram по компонентам Top, анализ Unreachable, анализ Soft Reference и Weak Reference, анализ Finalizer и т. д.

Dump Diff и Leak Suspects более наглядны, здесь мы поговорим о других ключевых моментах:

— Память Dump: при использовании jmap, arthas и других инструментов для создания снимков кучи не забудьте исключить трафик. Одновременно выполните дамп перед и после CMS GC.
— Анализ Top Component: обратите внимание на анализ объектов, классов, загрузчиков классов, пакетов и других аспектов. Также используйте outgoing и incoming для анализа связанных объектов. Затем проанализируйте Soft Reference и Weak Reference и Finalizer.
— Анализ Unreachable: сосредоточьтесь на этом, обращая внимание на размеры Shallow и Retained. Например, в одной оптимизации GC, основанной на Unreachable Objects, проблема скользящего окна Hystrix была обнаружена.

**Сцена 6: Длительное время однократного полного сбора CMS**

*Ситуация:*

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

*Причины:*

Во время процесса сбора CMS STW в основном включает этапы Init Mark и Final Remark, что также является основной причиной длительного времени полного сборщика CMS. Кроме того, иногда ожидание Mutator для достижения SafePoint перед STW также может привести к длительному времени, но это менее распространено. **Стратегия**

После того как мы выяснили два процесса STW, анализ и решение проблемы становятся проще. Поскольку большинство проблем возникает на этапе Final Remark, мы рассмотрим этот этап на примере. Основные шаги:

1. **Направление**: внимательно изучить журнал GC, найти журнал Final Remark во время возникновения проблемы, проанализировать обработку Reference и обработку метаданных real, сколько времени занимает. Для получения подробной информации необходимо использовать параметр `-XX:+PrintReferenceGC`. В основном в журнале можно определить, в каком направлении возникла проблема, если время превышает 10%, то нужно обратить внимание.

```shell
2019-02-27T19:55:37.920+0800: 516952.915: [GC (CMS Final Remark) 516952.915: [ParNew516952.939: [SoftReference, 0 refs, 0.0003857 secs]516952.939: [WeakReference, 1362 refs, 0.0002415 secs]516952.940: [FinalReference, 146 refs, 0.0001233 secs]516952.940: [PhantomReference, 0 refs, 57 refs, 0.0002369 secs]516952.940: [JNI Weak Reference, 0.0000662 secs]
[class unloading, 0.1770490 secs]516953.329: [scrub symbol table, 0.0442567 secs]516953.373: [scrub string table, 0.0036072 secs][1 CMS-remark: 1638504K(2048000K)] 1667558K(4352000K), 0.5269311 secs] [Times: user=1.20 sys=0.03, real=0.53 secs]
  1. Причина: после определения конкретного направления можно провести более глубокий анализ. Обычно наиболее вероятными причинами проблем являются этапы FinalReference и обработка метаданных в таблице символов scrub. Чтобы найти конкретную проблему кода, необходимо использовать инструменты анализа памяти MAT или JProfiler. Перед использованием инструментов MAT и т. д. можно также использовать команду для просмотра гистограммы объектов, возможно, можно сразу определить проблему.

    • Анализ этапа FinalReference в основном заключается в наблюдении за доминирующим деревом объектов java.lang.ref.Finalizer и определении источника утечки. Часто возникают проблемы с такими объектами, как Socket SocksSocketImpl, Jersey ClientRuntime, MySQL ConnectionImpl и т.д.
    • Таблица символов scrub означает очистку метаданных, занимающих время ссылки. Ссылка — это метод представления кода Java после компиляции в байт-код, жизненный цикл обычно совпадает с классом. Когда _should_unload_classes установлено в true, он обрабатывается вместе с классами и строковыми таблицами в CMSCollector::refProcessingWork().
  2. Стратегия: зная причину задержки GC, легче решить проблему. Такие проблемы не будут возникать одновременно в больших масштабах, но иногда время одного STW может быть довольно долгим. Если влияние на бизнес велико, рекомендуется своевременно снизить нагрузку на трафик и принять следующие меры оптимизации:

    • FinalReference: после нахождения источника памяти можно решить проблему путём оптимизации кода. Если невозможно быстро определить местоположение, можно увеличить -XX:+ParallelRefProcEnabled для параллельной обработки ссылок.
    • таблица символов: наблюдать за историческим пиковым использованием области MetaSpace и ситуацией до и после каждого GC. Как правило, нет динамической загрузки классов или обработки DSL, использование MetaSpace не будет сильно меняться, и эту проблему можно избежать, используя -XX:-CMSClassUnloadingEnabled, JDK8 по умолчанию включает CMSClassUnloadingEnabled, что приводит к тому, что CMS пытается выгрузить класс на этапе CMS-Remark.

Заключение

В нормальных условиях фоновый сборщик мусора CMS сталкивается с проблемами, которые в основном связаны с обработкой ссылок и метаданными. Что касается обработки ссылок, независимо от того, является ли это FinalReference, SoftReference, WeakReference, основным методом является определение момента создания снимка и последующее использование инструментов анализа памяти для анализа. В настоящее время нет хорошего способа обработки классов. В сборщике G1 также есть проблемы со ссылками, которые можно наблюдать в журнале Ref Proc, а методы обработки аналогичны CMS.

Сценарий семь: фрагментация памяти и деградация сборщика

  • Явление: параллельный алгоритм сборщика CMS деградирует до однопоточного последовательного алгоритма сборщика в режиме переднего плана, время STW становится очень большим, иногда достигая десятков секунд. Существует два типа однопоточных последовательных алгоритмов сборщика с деградацией CMS:

    • Алгоритм с сжатием, называемый MSC, который был представлен ранее, использует маркировку-очистку-сжатие, однопоточную полную приостановку для сбора всей кучи, то есть истинный полный сборщик мусора, приостановка времени больше, чем обычный CMS.
    • Без сжатия алгоритм собирает старый регион, который похож на обычный алгоритм CMS.
  • Причины: основные причины деградации сборщика CMS включают:

    • Неудачное продвижение (Promotion Failed).
    • Неудача инкрементного сбора (Incremental Collection Failure).
    • Явный сбор мусора (Explicit GC).
    • Сбой параллельного режима (Concurrent Mode Failure).
  • Стратегии: после анализа конкретной причины можно принять целенаправленные меры. Конкретная стратегия заключается в том, чтобы начать с причины и решить её.

    • Фрагментация памяти: используйте -XX:UseCMSCompactAtFullCollection=true, чтобы контролировать, выполнять ли сжатие пространства во время полного сбора мусора (по умолчанию включено, обратите внимание, что это полный сбор мусора, а не обычный сборщик CMS), а также -XX: CMSFullGCsBeforeCompaction=n, чтобы контролировать количество полных сборок мусора перед выполнением сжатия.
    • Инкрементальный сбор: уменьшите порог запуска сборщика CMS, то есть значение параметра -XX:CMSInitiatingOccupancyFraction, чтобы сборщик CMS мог запускаться раньше, обеспечивая достаточное непрерывное пространство и уменьшая использование пространства старого региона. Кроме того, необходимо использовать -XX:+UseCMSInitiatingOccupancyOnly, иначе JVM только устанавливает значение при первом использовании, а затем автоматически регулирует его.
    • Плавающий мусор: контролируйте размер объекта, который продвигается каждый раз, или сокращайте время каждого сборщика CMS. При необходимости отрегулируйте значение NewRatio. Кроме того, используйте -XX:+CMSScavengeBeforeRemark, чтобы заранее запустить молодой сборщик мусора во время процесса, чтобы предотвратить продвижение слишком большого количества объектов позже.
  • Заключение: в нормальных условиях сборщик CMS запускается в параллельном режиме, пауза очень короткая, влияние на бизнес невелико, но после деградации влияние будет очень большим. Рекомендуется полностью устранить проблему после обнаружения. Пока можно определить конкретные причины, связанные с фрагментацией памяти, плавающим мусором и инкрементным сбором, их всё ещё относительно легко решить. Что касается фрагментации памяти, если значение -XX:CMSFullGCsBeforeCompaction выбрано неправильно, вы можете использовать -XX:PrintFLSStatistics, чтобы наблюдать за уровнем фрагментации памяти, а затем установить конкретное значение. Наконец, при кодировании также следует избегать создания больших объектов, требующих непрерывного адресного пространства, таких как длинные строки, используемые для хранения вложений, сериализации или десериализации байтовых массивов, а также преждевременного продвижения.

Сценарий восемь: внешняя память кучи OOM

  • Явления: использование памяти продолжает расти, даже начинает использовать SWAP-память, и могут возникнуть ситуации, когда время GC резко возрастает, потоки блокируются и т. д., и через верхнюю команду обнаруживается, что размер RES процесса Java даже превышает -Xmx. Когда происходят эти явления, можно сделать вывод, что произошла утечка внешней памяти кучи.

  • Причины: две основные причины утечки внешней памяти кучи в JVM:

    • Через UnSafe#allocateMemory, ByteBuffer#allocateDirect активно запрашивает внешнюю память кучи и не освобождает её, часто встречается в NIO, Netty и других связанных компонентах.
    • Код имеет утечку памяти через вызов Native Code через JNI.
  • Стратегии:

    • Причина один: активно запрашивать и не освобождать.
    • Причина два: утечка памяти через Native Code, вызванная вызовом JNI.

Сценарий девять: проблемы GC, вызванные JNI

  • Явления: в журнале GC причина GC — GCLocker Initiated GC. Причины

JNI (Java Native Interface) — это интерфейс вызова Java-кода из нативного кода и наоборот. Существует два способа получения данных из JVM при использовании JNI: копирование данных или использование ссылок (указателей), что обеспечивает более высокую производительность.

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

Стратегии

  • Добавьте параметр -XX+PrintJNIGCStalls, чтобы получить информацию о потоках, вызывающих JNI, для дальнейшего анализа и определения проблемных вызовов JNI.
  • Будьте осторожны с использованием JNI, так как это не всегда приводит к повышению производительности и может вызвать проблемы со сборкой мусора.
  • Обновите JDK до версии 14, чтобы избежать повторной сборки мусора, связанной с JDK-8048556.

Оптимизация производительности JVM

Анализ нехватки дискового пространства

На самом деле, анализ нехватки дискового пространства относится к системному уровню и уровню программного обеспечения, а не к JVM. Однако, с другой стороны, нехватка дискового пространства также может вызывать аномалии в работе JVM.

Для анализа нехватки дискового пространства выполните следующие шаги:

  1. Используйте команду df -h для проверки состояния диска.
  2. Перейдите в каталог /.
  3. Выполните команду du -sh *, чтобы просмотреть размер файлов в каталоге.
  4. Найдите файлы с большим размером и удалите ненужные файлы.

Поиск процессов с высокой загрузкой процессора

Процесс поиска включает следующие шаги:

  • Используйте команду top для поиска идентификатора процесса.
  • Используйте top -Hp <pid> для поиска идентификаторов потоков с высокой загрузкой ЦП.
  • Преобразуйте идентификатор потока в шестнадцатеричный формат с помощью команды printf %x <pid>, чтобы использовать его в следующих шагах.
  • Выполните jstack -pid | grep -A 20 <pid>, чтобы найти информацию о стеке для каждого потока.
  • Проанализируйте стек вызовов, чтобы определить проблемный код.

Пример кода:

public class CPUSoaring {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (;;) {
                    System.out.println("I am children-thread1");
                }
            }
        }, "children-thread1");

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (;;) {
                    System.out.println("I am children-thread2");
                }
            }
        }, "children-thread2");

        thread1.start();
        thread2.start();
        System.err.println("I am is main thread!!!!!!!!");
    }
}

Анализ:

  1. Сначала используйте команду top, чтобы узнать, какой процесс занимает больше всего ресурсов процессора.
  2. Затем используйте top -Hp pid, чтобы найти поток с самой высокой загрузкой процессора.
  3. Преобразуйте идентификатор потока (tid) в шестнадцатеричное значение с помощью printf '%x\n' tid.
  4. Используйте jstack pid|grep xid -A 30, чтобы вывести информацию о стеке потока.
  5. Вы можете сохранить информацию о стеке в файл с помощью команды jstack -l pid > filename.txt.

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

Анализ переполнения памяти

Шаги анализа включают:

  • Поиск идентификатора процесса с помощью команды top -d 2 -c.
  • Просмотр распределения памяти в куче с помощью jmap -heap pid.
  • Анализ объектов с наибольшим использованием памяти с помощью jmap -histo pid | head -n 100.
  • Изучение живых объектов с наибольшим потреблением памяти с помощью jmap -histo:live pid | head -n 100.

Пример:

  • Сначала используйте top -d 2 -c, чтобы найти идентификатор процесса.
  • Затем выполните jmap -heap 8338, чтобы увидеть распределение памяти в куче.
  • Определите объекты с наибольшим использованием памяти.

Здесь вы можете увидеть количество объектов и их размеры.

Затем найдите объекты, которые занимают много памяти, и изучите, где они используются и почему существует такое большое количество объектов.

Анализ исключений OOM

Чтобы анализировать исключения OOM, установите параметры -XX:+HeapDumpOnOutOfMemoryError и -XX:HeapDumpPath=${каталог} при запуске сервиса. Это позволит автоматически создавать дампы кучи при возникновении исключений OOM. Затем можно провести анализ с помощью инструмента анализа памяти, такого как Visual VM.

Важно понимать, какие факторы могут привести к исключениям OOM с точки зрения JVM:

  • Java Heap
  • Method Area
  • Virtual Machine Stack
  • Native Method Stack
  • Program Counter
  • Direct Memory

Только область Program Counter не подвержена исключениям OOM. В Java 8 и выше метапространство (локальная память) может столкнуться с OOM. 而站在程序代码的角度来看,总结了大概有以下几点原因会导致OOM异常:

  • 内存泄露
  • 对象过大、过多
  • метод过长
  • 过度使用代理框架, 生成大量的类信息

接下来我们 рассмотрим OOM, появление OOM-исключения после dump-вывода кучи-дампа файла, затем открываем JDK-самописный Visual VM инструмент, импортируем кучу-дамп файл.

Я использую следующий код для OOM исключения:

import java.util.ArrayList;
import java.util.List;

class OOM {

    static class User{
        private String name;
        private int age;

        public User(String name, int age){
            this.name = name;
            this.age = age;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        List<User> list = new ArrayList<>();
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            Thread.sleep(1000);
            User user = new User("zhangsan" + i, i);
            list.add(user);
        }
    }
}

Код очень простой, он просто добавляет объекты в коллекцию, и после ввода в кучу-дамп файла можно увидеть экземпляры наиболее часто используемых классов в разделе «классы и экземпляры».

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

Вышеупомянутый метод — это способ поиска причины возникновения OOM после того, как уже произошло OOM-исключение. Это, безусловно, последний шаг в предотвращении. Как предотвратить возникновение OOM заранее?

Обычно крупные компании имеют собственные системы мониторинга, которые могут предоставлять информацию о состоянии здоровья (ЦП, память) в тестовых, предварительных и производственных средах обслуживания.

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

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

Jvisualvm

Проект часто выполняет YGC и FGC, проблема поиска и устранения

Память

Мониторинг использования памяти объектами и количества экземпляров.

ЦП

Отслеживание времени и частоты использования процессора.

Из графиков мониторинга можно сделать вывод, что основная часть времени тратится на сетевые запросы, но не на конкретный бизнес-код, потребляющий процессорное время.

Оптимизация распределения памяти кучи

Начальный размер кучи

  • Используйте настройки по умолчанию или отслеживайте использование памяти после стабильной работы системы в течение некоторого времени, чтобы определить начальный размер кучи.
  • На основе общих принципов распределения размера кучи, общий размер кучи должен быть в 3–4 раза больше размера старого поколения после полного GC.
  • Размер старого поколения должен быть в 2–3 раза больше размера после полного GC.
  • Размер молодого поколения должен составлять от 1 до 1,5 размера после полного GC.
  • Метапространство должно быть от 1,2 до 1,5 раза больше используемого метапространства после полного GC.

В Java куча используется для хранения активных объектов, то есть объектов, которые остаются в памяти во время выполнения приложения. После полного GC в старом поколении остаётся только небольшое количество долгоживущих объектов.

Точка начала и анализа размера кучи

  • Отслеживание продолжительности Minor GC.
  • Отслеживание количества Minor GC.
  • Отслеживание максимальной продолжительности Full GC.
  • Отслеживание частоты Full GC в наихудшем случае.
  • Основываясь на требованиях к задержке и пропускной способности бизнес-системы, можно оптимизировать размер каждого региона в соответствии с этими показателями.

Настройка молодого поколения

Правила настройки молодого поколения

  • Старый регион должен быть не менее чем в 1,5 раза больше активного набора. Размер старого региона должен составлять 2–3 размера активного набора.
  • Молодой регион должен занимать не менее 10% от общего размера кучи. Молодой регион должен иметь размер от 1 до 1,5 размеров старого активного набора.
  • При настройке молодого региона размер старого региона не должен изменяться.

Minor GC собирает мусор из молодого и выжившего регионов. Если молодой регион слишком мал, Minor GC будет происходить чаще. Если регион слишком велик, Minor GC может занять много времени и не соответствовать требованиям к задержкам бизнес-систем. Поэтому после настройки кучи проанализируйте журналы GC, чтобы убедиться, что частота и продолжительность Minor GC соответствуют требованиям бизнеса.

  • Причины частых Minor GC: если Minor GC происходит слишком часто, это означает, что молодого региона недостаточно, и новые объекты быстро заполняют его. Необходимо увеличить размер молодого региона.
  • Настройка молодого региона: после анализа журналов GC определите, сколько времени требуется для заполнения молодого региона, и настройте его размер соответствующим образом. Например, если молодой регион заполняется за 5 секунд, и вы хотите сократить частоту сбора до одного раза в 10 секунд, размер молодого региона можно увеличить до 256 МБ. Однако также необходимо увеличить размер старого региона, иначе может произойти частый полный сбор мусора.
  • Продолжительный Minor GC: продолжительный Minor GC указывает на то, что размер молодого региона слишком велик. Чтобы уменьшить продолжительность сбора, уменьшите размер молодого региона. Например, предположим, что Minor GC занимает 12,8 мс, а размер молодого региона составляет 192 МБ. Для сокращения продолжительности сбора до 10 мс размер молодого региона необходимо уменьшить до 160 МБ. При этом размер старого региона остаётся неизменным.

На основе журналов GC можно выполнить следующие расчёты:

Куча Молодой регион Сбор Продолжительность
512 МБ 192 МБ 11 141 мс
512 МБ 192 МБ 12 151 мс

Исходя из этих данных, можно настроить размер молодого региона следующим образом:

Куча Молодой регион Сбор Продолжительность
480 МБ 160 МБ 14 154 мс

Это снижает продолжительность сбора на 1,82 мс по сравнению с предыдущим значением. Однако цель в 10 мс ещё не достигнута. Продолжая логику расчёта, получаем:

1 / 11 = 0,09, то есть необходимо снизить продолжительность ещё на 9%.

Размер молодого региона уменьшается на 0,09 * 160 = 14,5 МБ, итоговый размер — 145 МБ. Общий размер кучи уменьшается на те же 14,5 МБ и становится равным 465 МБ.

Однако после настройки и проверки с помощью jmap -heap размер молодого региона оказывается больше ожидаемого (145 вместо 140 МБ). Это связано с настройкой параметра -XX:NewRatio, который по умолчанию равен 2 (соотношение молодого и старого регионов 1:2). В данном случае параметр был изменён на 3 (соотношение 1:3). Перевод текста на русский язык:

Конечный размер кучи памяти:

Рисунок 1.

Время MinorGC составило 159/16 = 9,93 миллисекунды.

Рисунок 2.

Время MinorGC составило 185/18 = 10,277 = 10,28 миллисекунды.

Рисунок 3.

Время MinorGC составило 205/20 = 10,25 миллисекунды.

Рисунок 4.

Проблема с длительным временем остановки MinorGC решена. MinorGC либо происходит довольно часто, либо время остановки довольно длительное. Решение этой проблемы заключается в настройке размера молодого поколения, но при настройке всё равно необходимо соблюдать следующие правила.

Оптимизация старого поколения

Оптимизация старого поколения проводится по той же схеме. Анализируется частота FullGC и время остановки, а затем производится настройка размера старого поколения в соответствии с установленными целями оптимизации.

Процесс оптимизации старого поколения:

  1. Анализ изменения использования пространства старым поколением после каждого MinorGC и вычисление размера объектов, которые были повышены до старого поколения после каждого MinorGC.
  2. На основе частоты MinorGC и размера объектов, повышенных до старого поколения, рассчитывается скорость увеличения, то есть сколько объектов повышается до старого поколения каждую секунду.
  3. После FullGC подсчитывается использование пространства старым поколением, и на основе расчёта скорости увеличения вычисляется, как долго старое поколение будет заполнено перед повторным возникновением FullGC. Также анализируются журналы FullGC для определения частоты FullGC. Если частота слишком высока, можно увеличить размер старого поколения для решения проблемы, сохраняя размер молодого поколения неизменным.
  4. Если частота FullGC соответствует цели оптимизации, но время остановки всё ещё слишком велико, можно рассмотреть использование сборщиков CMS или G1. Параллельный сбор может сократить время остановки.

Переполнение стека

Расследование исключений переполнения стека (включая виртуальный стек машины и локальный методный стек) аналогично расследованию OOM. Экспортируется информация о стеке исключения, а затем используется инструмент mat или Visual VM для анализа и поиска кода или метода, вызвавшего исключение.

Когда глубина запроса потока превышает допустимый размер виртуального стека машины, возникает исключение StackOverflowError. С точки зрения кода, причины слишком большой глубины запроса потока могут быть следующими: слишком большие или многочисленные объекты в методе, что приводит к слишком большому размеру таблицы локальных переменных, превышающему установленный параметр -Xss.

Расследование взаимоблокировок

Пример кода для демонстрации случая взаимоблокировки:

public class DeadLock {

    public static Object lock1 = new Object();
    public static Object lock2 = new Object();

    public static void main(String[] args){
        Thread a = new Thread(new Lock1(),"DeadLock1");
        Thread b = new Thread(new Lock2(),"DeadLock2");
        a.start();
        b.start();
    }
}
class Lock1 implements Runnable{
    @Override
    public void run(){
        try{
            while(true){
                synchronized(DeadLock.lock1){
                    System.out.println("Waiting for lock2");
                    Thread.sleep(3000);
                    synchronized(DeadLock.lock2){
                        System.err.println("Lock1 acquired lock1 and lock2 ");
                    }
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
class Lock2 implements Runnable{
    @Override
    public void run(){
        try{
            while(true){
                synchronized(DeadLock.lock2){
                    System.err.println("Waiting for lock1");
                    Thread.sleep(3000);
                    synchronized(DeadLock.lock1){
                               System.err.println("Lock2 acquired lock1 and lock2");
                    }
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

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

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

Для расследования взаимоблокировок, если вы находитесь в тестовой среде или локальной среде, вы можете использовать Visual VM для подключения к процессу, и он автоматически обнаружит наличие взаимоблокировки.

И вы можете просмотреть информацию о стеке потоков, чтобы увидеть конкретные потоки, находящиеся в тупике.

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

Arthas может использовать команду thread для расследования взаимоблокировок. Следует обратить внимание на состояние BLOCKED потоков.

Конкретные параметры thread можно найти на следующем рисунке.

Как избежать взаимоблокировок?

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

  • Во-первых, избегать последовательности блокировки ресурсов, которую поддерживают оба потока.
  • Кроме того, избегайте конкуренции за несколько ресурсов одним потоком.
  • Также устанавливайте время истечения срока действия для уже полученных ресурсов блокировки, чтобы предотвратить аномалии и отсутствие освобождения ресурсов блокировки. Вы можете использовать метод acquire() для указания параметра timeout при блокировке ресурсов.
  • Наконец, используйте сторонние инструменты для обнаружения взаимоблокировок и предотвращения их возникновения в онлайн-режиме.

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

Практическая оптимизация

Выше мы говорили об оптимизации целей и показателей, поэтому давайте перейдём к практической оптимизации. Сначала подготовим пример кода:

import java.util.ArrayList;
import java.util.List;

class OOM {

    static class User{
        private String name;
        private int age;

        public User(String name, int age){
            this.name = name;
            this.age = age;
        }

    }

    public static void main(String[] args) throws InterruptedException {
        List<User> list = new ArrayList<>();
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
             Tread.sleep(1000);
            System.err.println(Thread.currentThread().getName());
            User user = new User("zhangsan"+i,i);
            list.add(user);

``` **Оптимизация параметров JVM**

В данном тексте описывается процесс оптимизации параметров Java Virtual Machine (JVM) для улучшения производительности и стабильности приложения. Автор приводит примеры настройки различных параметров, таких как размер кучи, размер стека, параметры GC и другие, а также анализирует результаты этих настроек.

Автор начинает с описания проблемы, связанной с частыми Minor GC и Full GC, которые приводят к снижению производительности приложения. Затем он предлагает несколько вариантов настройки параметров JVM для решения этой проблемы.

Первый вариант настройки включает увеличение размера кучи и использование GC LogFileRotation. Это позволяет уменьшить частоту Minor GC, но не решает проблему полностью.

Второй вариант настройки включает изменение размера метапространства и использование CMS GC. Это также помогает уменьшить частоту GC, но приводит к увеличению времени паузы при GC.

Третий вариант настройки включает дальнейшее увеличение размера метапространства, изменение размера кучи и использование CMS GC. В результате достигается стабильная работа приложения без частых GC.

Однако автор подчёркивает, что оптимизация параметров JVM  это сложный процесс, который требует тщательного анализа и тестирования. Он также отмечает, что нет универсального решения для всех приложений, и каждый случай требует индивидуального подхода.

Далее автор описывает различные параметры JVM, такие как размер кучи, размер стека, размер метапространства и другие. Он объясняет, как эти параметры влияют на работу приложения и какие значения следует использовать в зависимости от конкретных требований.

Также автор упоминает о различных сборщиках мусора, таких как Serial, Parallel Scavenge, Parallel Old, ParNew и CMS. Он описывает их особенности и рекомендует использовать их в зависимости от типа приложения и требований к производительности.

В заключение автор подчёркивает важность оптимизации параметров JVM и призывает к тщательному анализу и тестированию перед внесением изменений в конфигурацию. -XX:+UseCMSCompactAtFullCollection используется для включения процесса объединения фрагментов памяти при необходимости выполнения полного GC (сборщика мусора) в CMS-сборщике. До появления Shenandoah и ZGC этот процесс был непараллельным.

-XX:CMSFullGCsBeforeCompaction определяет, сколько полных GC должно произойти перед выполнением процесса сжатия после. Значение по умолчанию  0, что означает выполнение процесса сжатия при каждом полном GC.

-XX:CMSInitiatingOccupancyFraction устанавливает долю использования старого поколения, при достижении которой будет инициирован полный GC. По умолчанию это значение равно 92.

-XX:+UseCMSInitiatingOccupancyOnly используется вместе с предыдущим параметром. Он определяет, будет ли постоянно использоваться установленное значение доли использования старого поколения для инициирования полного GC или же будет происходить автоматическая адаптация.

-XX:+CMSScavengeBeforeRemark включает запуск Minor GC перед полным GC для уменьшения количества ссылок на старое поколение из молодого. Это снижает нагрузку на этап маркировки в процессе полного GC CMS. Обычно около 80% времени полного GC тратится на этап маркировки.

-XX:+CMSParallelRemarkEnabled позволяет выполнять этап повторной маркировки параллельно, чтобы уменьшить время остановки системы (STW).

#### G1垃圾收集器

-XX:+UseG1GC включает сборщик мусора G1.

-XX:-UseG1GC отключает сборщик мусора G1.

-XX:G1HeapRegionSize устанавливает размер каждого региона в куче. Значения могут варьироваться от 1 МБ до 32 МБ.

-XX:MaxGCPauseMillis устанавливает максимальное время паузы для сборщика мусора. По умолчанию оно составляет 200 миллисекунд. Обычно рекомендуется устанавливать ожидаемое время паузы в диапазоне от 100 до 300 миллисекунд для оптимального управления сборщиком мусора. **jstack**

jstack(Java Stack Trace) в основном используется для печати информации о стеке потоков, это мощный инструмент анализа потоков, предоставляемый JDK, который может помочь нам анализировать состояние потока, взаимоблокировку и т. д. во время выполнения программы.

```shell
# Выгрузить дамп процесса <pid> в файл /data/1.log
jstack -l <pid> >/data/1.log

# Описание параметров:
# -F: Если нормальное выполнение команды jstack не отвечает (например, процесс завис), можно добавить этот параметр для принудительного выполнения thread dump
# -m: Помимо вывода трассировки стека методов Java, также будет выводиться информация о стеке нативных методов
# -l: Выводит дополнительную информацию, связанную с блокировкой. Использование этого параметра может привести к увеличению времени остановки JVM, и его следует использовать с осторожностью в производственной среде

В файле дампа jstack стоит обратить внимание на следующие состояния потока:

  • Взаимоблокировка (Deadlock) — основное внимание
  • Выполнение (Runnable)
  • Ожидание ресурса (Waiting on condition) — основное внимание
    • Ожидание определённого ресурса или условия для пробуждения. Необходимо проанализировать конкретную ситуацию, например, поток спит, а сетевое чтение и запись заняты ожиданием
    • Если большое количество потоков находится в состоянии «ожидания условия», и они ожидают сетевые ресурсы, это может быть признаком узкого места в сети
  • Ожидание получения монитора (Waiting on monitor entry) — основное внимание
    • Если большое количество потоков находятся в состоянии «waiting for monitor entry», возможно, глобальная блокировка заблокировала большое количество потоков
  • Приостановленное (Suspended)
  • Объект ожидает (Object.wait() или TIMED_WAITING)
  • Блокировка (Blocked) — основное внимание
  • Остановка (Parked)

Примечание: если постоянно появляется один и тот же call stack, у нас есть более 80% причин полагать, что в этом коде есть проблема с производительностью (исключая часть чтения сети).

Сценарий 1: Анализ проблемы BLOCKED

"RMI TCP Connection(267865)-172.16.5.25" daemon prio=10 tid=0x00007fd508371000 nid=0x55ae waiting for monitor entry [0x00007fd4f8684000]
   java.lang.Thread.State: BLOCKED (on object monitor)
at org.apache.log4j.Category.callAppenders(Category.java:201)
- waiting to lock <0x00000000acf4d0c0> (a org.apache.log4j.Logger)
at org.apache.log4j.Category.forcedLog(Category.java:388)
at org.apache.log4j.Category.log(Category.java:853)
at org.apache.commons.logging.impl.Log4JLogger.warn(Log4JLogger.java:234)
at com.tuan.core.common.lang.cache.remote.SpyMemcachedClient.get(SpyMemcachedClient.java:110)
  • Состояние потока — Blocked, заблокированное состояние. Это указывает на то, что поток ожидает ресурс дольше установленного времени.
  • «waiting to lock <0x00000000acf4d0c0>» означает, что поток пытается получить блокировку по адресу 0x00000000acf4d0c0 (на английском языке можно описать как попытку получить блокировку 0x00000000acf4d0c0).
  • В журнале дампа найдите строку 0x00000000acf4d0c0 и обнаружите, что большое количество потоков ожидает блокировки по этому адресу. Если можно найти, кто получил эту блокировку в журнале (например, locked < 0x00000000acf4d0c0 >), мы можем легко найти причину.
  • «Ожидание входа в монитор» указывает на то, что этот поток запросил доступ к критической секции через synchronized(obj) {…}, поэтому он вошёл в очередь Entry Set, но другой поток владеет монитором, поэтому текущий поток ожидает в очереди Entry Set.
  • Первая строка, «RMI TCP Connection (267865) -172.16.5.25», является именем потока. tid указывает идентификатор потока Java. nid указывает идентификатор собственного потока. prio — приоритет потока. [0x00007fd4f8684000] — начальный адрес стека потока.

Сценарий 2: Анализ проблемы с высокой загрузкой ЦП

  1. Используйте команду top для поиска наиболее загруженного процесса (Shift + P).
  2. Просмотрите высоконагруженный процесс и найдите высоконагруженные потоки (top -Hp или ps -mp -o THREAD,tid,time).
  3. Найдите наиболее загруженный поток и запишите thread_id, преобразуйте номер потока в шестнадцатеричный формат (printf "%X\n" thread_id).
  4. (Необязательно) Выполните просмотр имени высоконагруженного потока (jstack 16143 | grep 3fb6).
  5. Экспортируйте журнал стека процессов и найдите номер 3fb6 потока (jstack 16143 >/home/16143.log).
  6. Свяжите найденную информацию о стеке с кодом для анализа и определения причины.

jmap

jmap (Java Memory Map) в основном используется для отображения информации о памяти. Общие команды:

jmap -dump:live,format=b,file=xxx.hprof <pid>

Просмотр использования кучи JVM

[root@localhost ~]# jmap -heap 7243
Attaching to process ID 27900, please wait...
Debugger attached successfully.
Client compiler detected.
JVM version is 20.45-b01
using thread-local object allocation.
Mark Sweep Compact GC
Heap Configuration: # Настроить конфигурацию памяти кучи
   MinHeapFreeRatio = 40     #-XX:MinHeapFreeRatio устанавливает минимальный коэффициент свободного пространства кучи JVM
   MaxHeapFreeRatio = 70   #-XX:MaxHeapFreeRatio установить максимальный коэффициент свободного пространства кучи JVM
   MaxHeapSize = 100663296 (96,0 МБ)   #-XX:MaxHeapSize=установить максимальный размер кучи JVM
   NewSize = 1048576 (1,0 МБ)     #-XX:NewSize=установить размер по умолчанию для нового поколения кучи JVM
   MaxNewSize = 4294901760 (4095,9375 МБ) #-XX:MaxNewSize=установить максимальный размер нового поколения кучи JVM
   OldSize = 4194304 (4,0 МБ)  #-XX:OldSize=установить размер старого поколения кучи JVM
   NewRatio = 2    #-XX:NewRatio=установить соотношение размеров нового и старого поколений кучи JVM
   SurvivorRatio = 8  #-XX:SurvivorRatio=установить отношение размеров Эдема и Выжившего в новом поколении кучи JVM
   PermSize = 12582912 (12,0 МБ) #-XX:PermSize=<значение>: установить начальный размер пространства пермитива кучи JVM
   MaxPermSize = 67108864 (64,0 МБ) #-XX:MaxPermSize=<value>: установить максимальный размер пространства пермитива кучи JVM
Heap Usage:
Новое поколение (Eden + 1 Survivor Space): # Распределение памяти нового поколения, включая область Эдем + 1 область Выжившего
   capacity = 30212096 (28,8125 МБ)
   used = 27103784 (25,848182678222656 МБ)
   free = 3108312 (2,9643173217773438 МБ)
   89,71169693092462% использовано
Область Эдема: # Распределение памяти области Эдема
   capacity = 26869760 (25,625 МБ)
   used = 26869760 (25,625 МБ)
   free = 0 (0,0 МБ)
   100,0% использовано
Из космоса: # распределение памяти одной из областей Выживших
   capacity = 3342336 (3,1875 МБ)
   used = 234024 (0,22318267822265625 МБ)
   free = 3108312 (2,9643173217773438 МБ)
   7,001809512867647% использовано
Для космоса: # распределение памяти другой области Выживших
   capacity = 3342336 (3,1875 МБ)
``` **jhat**

Jhat (JVM Heap Analysis Tool)  это команда, которая используется вместе с jmap для анализа дампа, созданного с помощью этой команды. Jhat включает в себя встроенный микро-HTTP/HTML сервер, который позволяет просматривать результаты анализа после генерации дампа.

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

```powershell
# Анализ Java-дампа и запуск веб-сервера
jhat heapDump.dump

jconsole

jconsole (Java Monitoring and Management Console) — это графический инструмент мониторинга Java, который отображает различные данные в виде графиков. Он также позволяет удалённо отслеживать виртуальные машины через соединение. Это удобное средство мониторинга виртуальных машин, которое обладает мощными функциями. Для запуска jconsole достаточно ввести команду в командной строке.

Шаг 1: В файле catalina.sh в каталоге tomcat на удалённом компьютере добавьте следующие настройки:

JAVA_OPTS="$JAVA_OPTS -Djava.rmi.server.hostname=192.168.202.121 -Dcom.sun.management.jmxremote"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=12345"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.authenticate=true"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.ssl=false"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.pwd.file=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.101-3.b13.el7_2.x86_64/jre/lib/management/jmxremote.password"

Шаг 2: Настройте файлы полномочий:

[root@localhost bin]# cd /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.101-3.b13.el7_2.x86_64/jre/lib/management/
[root@localhost management]# cp jmxremote.password.template jmxremote.password
[root@localhost management]# vi jmxremote.password

Добавьте строки «monitorRole QED» и «controlRole chenqimiao».

Шаг 3: Установите права доступа к файлам полномочий:

[root@localhost management]# chmod 600 jmxremote.password jmxremote.access

На этом основная настройка завершена. Далее необходимо решить проблемы с брандмауэром и настройкой IP-адреса:

  1. Откройте порт 12345 в брандмауэре.
  2. Измените IP-адрес с 127.0.0.1 на реальный IP-адрес сервера (в данном случае 192.168.202.121).

Шаг 4: Запустите JConsole:

После выполнения этих шагов можно использовать JConsole для мониторинга виртуальной машины.

jvisualvm

jvisualvm (JVM Monitoring/Troubleshooting/Profiling Tool) — ещё один графический инструмент для мониторинга Java. Он позволяет просматривать локальные и удалённые виртуальные машины. Использование jvisualvm аналогично jconsole.

Для использования jvisualvm просто введите команду в командной строке. Однако jvisualvm предоставляет более удобный интерфейс по сравнению с jconsole, а данные отображаются в реальном времени.

Процесс подключения к удалённой виртуальной машине с использованием jvisualvm аналогичен процессу с jconsole. Конечный результат выглядит следующим образом:

Visual GC (мониторинг сборщика мусора)

По умолчанию Visual GC не установлен в Java VisualVM. Его необходимо установить вручную. Перейдите в каталог bin JDK и дважды щёлкните jvisualvm.sh, чтобы открыть Java Visual VM. Затем перейдите в меню «Инструменты → Плагины» и установите Visual GC.

Большие дамп-файлы

Если после создания дампа памяти файл становится слишком большим (около 5,5 ГБ), загрузка файла и просмотр экземпляров объектов могут занять много времени, и может появиться сообщение о необходимости настройки размера XMX. Это указывает на то, что VisualVM не хватает выделенной памяти.

Чтобы решить эту проблему, найдите файл visualvm.conf в каталоге $JAVA_HOME/lib/visualvm/etc и измените строку default_options на:

default_options="-J-Xms24m -J-Xmx192m"

Затем перезапустите VisualVM.

jmc

Java Mission Control (jmc) — это инструмент графического мониторинга, предоставляемый JDK. Он предоставляет обширную информацию о мониторинге. После открытия журнала производительности jmc включает следующие функции: общие сведения, память, код, потоки, ввод-вывод, система и события.

Основная особенность jmc — Java Flight Recorder (JFR), основанный на Java регистратор полётов. Данные JFR представляют собой историческую запись событий JVM, которые можно использовать для диагностики производительности и работы JVM. Эти данные можно анализировать с помощью jmc. Процесс_id JFR.dump [options_list]

Параметры options_list:

  • name=name — имя записи;
  • recording=n — номер записи JFR (случайное число, идентифицирующее запись);
  • filename=path — путь к файлу сохранения дампа.

Посмотреть процессы со всеми записями:

jcmd process_id JFR.check [verbose]

Записи различаются по именам, JVM также присваивает каждой записи случайный номер.

Остановить запись:

jcmd process_id JFR.stop [options_list]

Параметры options_list:

  • name=name — название записи для остановки;
  • recording=n — идентификатор записи для остановки;
  • discard=boolean — если true, данные будут удалены, а не записаны в указанный файл;
  • filename=path — имя файла для записи данных.

Запуск JFR на примере

  1. Создайте файл шаблона JFR (template.jfc). Для этого запустите jmc и выберите пункт меню Window->Flight Recording Template Manage. После подготовки файла экспортируйте его и перенесите в среду, где требуется устранить проблему.

  2. Поскольку JFR требует коммерческого сертификата JDK, необходимо разблокировать коммерческие функции JDK. Выполните команду:

    [root@localhost bin]# jcmd 12234 VM.unlock_commercial_features
    12234: Commercial Features already unlocked.
  3. Запустите JFR с помощью команды:

    jcmd <PID> JFR.start name=test duration=60s [settings=template.jfc] filename=output.jfr

    Команда немедленно запускает JFR и начинает использовать конфигурацию template.jfc, чтобы собирать информацию о JVM в течение 60 секунд, и записывать её в файл output.jfr. После завершения записи можно скопировать файл .jfr в рабочую среду и проанализировать его с помощью jmc GUI. Файл содержит почти всю необходимую информацию для устранения проблем с JVM, включая информацию об исключениях во время дампа кучи.

Пример использования:

[root@localhost bin]# jcmd 12234 JFR.start name=test duration=60s filename=output.jfr
12234: Started recording 6. The result will be written to: /root/zookeeper-3.4.12/bin/output.jfr
[root@localhost bin]# ls -l
-rw-r--r-- 1 root root 298585 6 июня 29 11:09 output.jfr

JFR (Java Flight Recorder) — основная функция Java Mission Control. JFR записывает историю событий JVM, которые могут быть использованы для диагностики производительности и работы JVM.

Основные операции JFR включают активацию определённых событий (например, блокирование потока из-за ожидания блокировки). Когда происходит активированное событие, связанные с ним данные записываются в память или файл на диске. Буфер событий является циклическим, и только последние события могут быть извлечены из него. Java Mission Control может отображать эти события на интерфейсе (из памяти JVM или файлов событий), что позволяет анализировать производительность JVM на основе этих событий.

Типы событий, размер буфера, способ хранения данных и другие параметры контролируются через параметры JVM, интерфейс Java Mission Control и команды jcmd. JFR обычно компилируется в программу, так как его влияние на приложение незначительно и обычно составляет менее 1%. Однако увеличение количества событий или изменение порога регистрации могут увеличить нагрузку на JFR.

На примере GlassFish Web Server, на котором работает Servlet из главы 2, после загрузки данных, собранных JFR, Java Mission Control будет выглядеть примерно так:

Здесь можно увидеть информацию о CPU, использовании кучи, свойствах JVM, системных свойствах и состоянии записи JFR.

Java Mission Control предоставляет множество информации, но на этом рисунке показана только одна метка. На рисунке видно, что использование памяти JVM часто меняется, поскольку молодое поколение регулярно очищается (интересно, что размер головы не увеличивается). Панель слева показывает недавнюю активность сборки мусора, включая продолжительность GC и тип GC. Если вы щёлкнете на событии, правая панель покажет детали этого события, включая этапы GC и статистику. Из меток панели видно, что есть много другой информации, такой как количество удалённых объектов, время, затраченное на удаление, конфигурация алгоритма GC, информация о распределении объектов и т. д. В главах 5 и 6 мы рассмотрим это более подробно.

Эта диаграмма также имеет много вкладок, показывающих частоту использования каждого пакета, использование классов, исключения, компиляцию, кэш кода, загрузку классов и т.д.:

Этот график также содержит много информации. Он показывает обзор событий: Анализ GC Roots для отслеживания ссылок

Если вы не можете определить разницу при просмотре больших объектов, можно попробовать сгруппировать их по классам и затем искать подозрительные объекты для анализа цепочки ссылок GC.

Можно сразу просмотреть цепочку ссылок GC, указав опцию Merge Shortest Path to GCRoots. Эта опция не совсем понятна, но она также может помочь найти классы с цепочками ссылок GC. Точность этого метода ещё предстоит проверить.

Практический пример 1:

Таким образом, иногда анализ MAT требует некоторого опыта, который поможет вам быстрее и точнее определить местоположение.

  • Практический пример 2: Анализ использования коллекций Коллекции часто используются в разработке. Важно выбрать подходящую структуру данных для коллекции, определить начальную ёмкость (слишком маленькая может привести к частым расширениям, а слишком большая — к дополнительным затратам памяти). Если эти вопросы не ясны или вы хотите посмотреть на использование коллекции, вы можете использовать MAT для анализа.

    • Выбор целевых объектов: Рисунок 1.

    • Show Retained Set: поиск объектов, которые будут собраны сборщиком мусора, когда X будет собран. Рисунок 2.

    • Группировка указанных объектов (Hash Map, ArrayList) по размеру: Рисунок 3.

    • Просмотр непосредственных доминаторов указанного класса: Рисунок 4.

Коэффициент заполнения коллекций: этот метод позволяет просматривать только те коллекции с предварительной памятью, такие как HashMap и ArrayList. Расчёт коэффициента: «размер / вместимость».

Рисунок 5.

Рисунок 6.

  • Практический пример 3: Анализ производительности Hash Когда в коллекции Hash слишком много объектов возвращают одно и то же значение Hash, это серьёзно влияет на производительность (принцип алгоритма Hash см. самостоятельно). Здесь мы ищем виновника высокого уровня столкновений в коллекциях Hash.

    • Коэффициент столкновений карты: проверка каждого экземпляра HashMap или HashTable и сортировка по коэффициенту столкновений. Коэффициент столкновений = количество столкнувшихся объектов / общее количество объектов в таблице Hash. Рисунок 7.

    • Просмотр непосредственных доминаторов: Рисунок 8.

      Рисунок 9.

    • Использование HashEntries для просмотра ключа и значения: Рисунок 10.

    • Аналогичный анализ других коллекций:

  • Практический пример 4: Быстрый поиск неиспользуемых коллекций с помощью OQL

    • Поиск пустых и неизменённых коллекций с использованием OQL:
select * from java.util.ArrayList where size=0 and modCount=01
select * from java.util.HashMap where size=0 and modCount=0
select * from java.util.Hashtable where count=0 and modCount=012

Рисунок 11.

  • Непосредственные доминаторы (просмотр ссылающихся объектов): Рисунок 12.

  • Расчёт размера Retained для пустых коллекций и просмотр объёма используемой памяти: Рисунок 13.

🔥 Огненный график

Огненный график — это инструмент для анализа узких мест в работе программы. Огненные графики также могут использоваться для анализа Java-приложений.

Установка среды

Убедитесь, что ваша машина уже имеет git, JDK, Perl, компилятор C++.

Установка Perl

wget http://www.cpan.org/src/5.0/perl-5.26.1.tar.gz
tar zxvf perl-5.26.1.tar.gz
cd perl-5.26.1
./Configure -de
make
make test
make install

После установки процесс занимает некоторое время. После завершения установки вы можете проверить успешность установки, используя команду perl -version.

Компилятор C++

apt-get install g++

Обычно используется для компиляции программ на C++. При компиляции программы на C++ с помощью команды make без компилятора C++, вы получите ошибку «g++: not found».

Клонирование соответствующих проектов

Скачайте два проекта, которые вам нужны (рекомендуется поместить их в каталог data):

git clone https://github.com/jvm-profiling-tools/async-profiler
git clone https://github.com/brendangregg/FlameGraph

Сборка проекта

После загрузки откройте файл async-profiler и введите команду make для сборки:

cd async-profiler
make

Создание файлов

Создание данных огненного графика

Вы можете загрузить сжатый пакет async-profiler с GitHub для выполнения соответствующих операций. Перейдите в каталог проекта async-profiler, затем введите следующую команду:

./profiler.sh -d 60 -o collapsed -f /tmp/test_01.txt ${pid}

Здесь -d обозначает продолжительность сбора данных, 60 означает сбор данных в течение 60 секунд, -o обозначает формат сбора данных, здесь используется collapsed, -f указывает путь к файлу данных после сбора (здесь данные сохраняются в файле /tmp/test_01.txt), а ${pid} обозначает целевой процесс сбора pid. После запуска программа блокируется до завершения сбора данных. Проверьте, есть ли соответствующий файл в каталоге tmp после завершения работы.

Генерация файла SVG

Содержимое файла, созданного на предыдущем шаге, трудно понять невооружённым глазом, поэтому теперь вступает в действие FlameGraph. Он может считывать этот файл и создавать понятный огненный график. Теперь перейдите в каталог этого проекта и выполните следующую команду:

perl flamegraph.pl --colors=java /tmp/test_01.txt > test_01.svg

Поскольку это файл Perl, используйте команду Perl для запуска этого файла. За флагом --colors следует стиль цвета, здесь java, за которым следует путь к файлу данных, здесь файл /tmp/test_01.txt, созданный на предыдущем этапе, и, наконец, имя выходного файла test_01.svg и его расположение. После выполнения команды вы увидите, что файл создан в текущем каталоге.

Просмотр огненных графиков

Теперь загрузите этот файл, откройте его в браузере и просмотрите результат:

![Огненный график](images/JVM/огненный график.svg)

Пример огненного графика

Пример сгенерированного [огненного графика](images/DevOps/огненный график.svg):

![Пример огненного графика](images/JVM/пример огненного графика.jpg)

Узкое место 1

CoohuaAnalytics$KafkaConsumer:::send метод содержит относительно большое количество операций сжатия Gzip. Мы определили узкое место на уровне метода, что значительно упрощает поиск конкретной проблемы. Мы сразу находим основную причину: сжатие Gzip.

![Узкое место 1](images/JVM/узкое место 1.jpg)

Узкое место 2

Разверните волну 2, чтобы увидеть, что метод getOurStackTrace занимает значительную часть процессорного времени. Это предполагает, что код часто использует исключения для получения текущего стека вызовов.

Посмотрите на код:

Действительно, мы обнаружили вторую основную причину потребления ресурсов процессора: new Exception().getStackTrace().

Узкое место 3

Разверните третью волну и увидите, что она связана с распаковкой Gzip:

Найдите соответствующий код и обнаружите, что каждый параметр запроса подвергается распаковке Gzip:

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/yu120-lemon-guide.git
git@api.gitlife.ru:oschina-mirror/yu120-lemon-guide.git
oschina-mirror
yu120-lemon-guide
yu120-lemon-guide
main