Class файл, константный пул
Class файл — это набор двоичных данных, организованных в виде потока байтов. Во время компиляции кода Java файлы преобразуются в формат class и сохраняются на диске в виде двоичных данных. В этот формат входит константный пул.
— Константный пул — это область памяти, которая содержит информацию о константах, используемых в классе. Эта информация может включать строковые литералы, имена классов и методов, а также другие константы.
Характеристики константного пула:
Пул времени выполнения
Пул времени выполнения (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, которое теперь контролируется доступным пространством локальной памяти.
Стратегия распределения памяти
Объекты обычно распределяются следующим образом:
Основная стратегия параметров
Размер каждой области влияет на производительность 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:
Если ошибка возникает при запуске приложения, можно увеличить размер пространства Permgen, изменив параметр -XX:MaxPermSize.
Если ошибка появляется при повторном развёртывании приложения, возможно, проблема связана с тем, что классы не были перезагружены. В этом случае достаточно перезапустить JVM.
Если ошибка возникает во время работы приложения, вероятно, оно динамически создаёт большое количество классов с коротким жизненным циклом. По умолчанию 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, которое можно изменить только путём перезагрузки.
— Недостаточно собственной памяти.
Обычно процесс протекает следующим образом:
Приложение внутри JVM запрашивает создание нового потока Java.
Собственный метод JVM проксирует этот запрос и пытается создать собственный поток через операционную систему.
Операционная система пытается создать новый поток и выделить для него память.
Если виртуальная память операционной системы исчерпана или есть ограничения из-за 32-битного процесса, операционная система отклоняет запрос на выделение памяти.
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) и исследует все объекты, которые могут быть достигнуты из этих корней. Корни включают:
Если объект недоступен из корней, он считается мусором и может быть удалён сборщиком мусора (Garbage Collector).
Сбор мусора
Сбор мусора включает два основных этапа: маркировку (Mark) и очистку (Sweep). На этапе маркировки сборщик мусора находит все активные объекты. Затем на этапе очистки удаляются все немаркированные объекты.
Существуют также другие алгоритмы сбора мусора, такие как копирование (Copying), маркировка-очистка (Mark-Sweep) и маркировка-компактирование (Mark-Compact). Алгоритм копирования копирует все живые объекты в новое пространство памяти, освобождая старое пространство для повторного использования. Алгоритмы маркировки-очистки и маркировки-компактирования также используют маркировку для определения живых объектов, но затем выполняют очистку или компактирование соответственно.
В Java Virtual Machine (JVM) используются различные алгоритмы сбора мусора в зависимости от поколения объектов. Молодые объекты (Young Generation) обычно используют алгоритм копирования, а старые объекты (Old Generation) — алгоритмы маркировки-очистки или маркировки-компактирования. Эден: разделение и сбор
Когда Эден заполняется, запускается Minor GC молодого поколения. Процесс выглядит следующим образом:
В этом процессе один из Survivor разделов всегда остаётся пустым. По умолчанию соотношение Eden, from и to составляет 8:1:1, что приводит к 10% потере пространства. Это соотношение настраивается параметром -XX:SurvivorRatio (по умолчанию 8).
Старое поколение (Old/Tenured Generation)
Для старого поколения обычно используются алгоритмы «маркировка-очистка» и «маркировка-сборка», поскольку выживаемость объектов в старом поколении высока, а пространство велико. Объекты попадают в старое поколение следующим образом:
Разделение на разделы
GC-сборщик мусора имеет различные конфигурации для молодого и старого поколений. Вот некоторые из них:
Сборщик молодого поколения
Существует несколько типов сборщиков для молодого поколения:
Serial: обрабатывает сборку мусора только одним потоком, останавливая все пользовательские потоки во время процесса. Самый простой сборщик, но он всё ещё полезен. Он используется в клиентских приложениях, где создание большого количества объектов не происходит часто, и пользователи не замечают значительных пауз. Этот сборщик потребляет меньше ресурсов и является более лёгким.
ParNew: многопоточная версия Serial. Несколько потоков GC выполняют сборку мусора параллельно. Процесс по-прежнему требует остановки пользовательских потоков. ParNew стремится сократить время паузы, предлагая улучшенную производительность по сравнению с Serial в многопроцессорных средах. Однако переключение между потоками требует дополнительных затрат, поэтому в однопроцессорной среде ParNew может уступать Serial.
Parallel Scavenge: ещё одна многопоточная версия сборщика мусора. Основное отличие от ParNew заключается в том, что Parallel Scavenge ориентирован на пропускную способность процессора и способен быстро завершать задачи, что подходит для фоновых вычислений без взаимодействия с пользователем.
Сборщики старого поколения
-XX:+UseParNewGC
включает использование CMS.Процесс работы CMS включает начальную маркировку (только прямые ссылки на GC Roots), параллельную маркировку (поиск всех достижимых объектов) и повторную маркировку (исправление изменений, вызванных работой приложения во время параллельной маркировки). Затем следует параллельная очистка.
CMS запускается, когда использование старого поколения достигает 80%. Параметры -XX:CMSInitiatingOccupancyFraction=80
и -XX:+UseCMSInitiatingOccupancyOnly
управляют этим процессом.
Преимущества CMS включают параллельный сбор и минимальные паузы. Недостатки включают конкуренцию за ресурсы процессора, невозможность сбора плавающего мусора и риск сбоя параллельного режима.
Сборщик мусора G1 реализует алгоритм, основанный на маркировке и копировании. Сборщик мусора работает в четыре этапа:
Начальная маркировка: только помечает объекты, которые могут быть напрямую связаны с корнями сборщика мусора GC Roots, и изменяет указатель TAMS. Этот этап требует остановки потоков, но он очень короткий и синхронизируется с Minor GC, поэтому сборщик мусора не имеет дополнительной паузы на этом этапе.
Параллельная маркировка: начинается с корней сборщика мусора и анализирует достижимость объектов в куче. Рекурсивно сканирует всю диаграмму объектов кучи, находит объекты для сбора и обрабатывает изменения ссылок между этапами. Этот этап может выполняться параллельно с пользовательскими потоками.
Окончательная маркировка: короткая пауза для пользовательских потоков после завершения параллельной маркировки. Обрабатывает изменения ссылок, оставшиеся после параллельной фазы.
Фаза очистки: обновляет статистические данные о регионах, сортирует регионы по стоимости и затратам на сбор, планирует сбор на основе ожидаемого времени паузы пользователя и выбирает несколько регионов для сбора. Копирует живые объекты из выбранных регионов в пустые регионы и очищает все пространство старого региона. Эта операция включает перемещение живых объектов и требует паузы для пользовательских потоков. Она выполняется несколькими потоками сборщика мусора параллельно.
В памяти кучи сборщика мусора G1 разделена на несколько равных блоков памяти, называемых регионами. Каждый регион представляет собой логически непрерывный блок памяти.
Регион
Размер каждого региона можно указать с помощью параметра -XX:G1HeapRegionSize
. Размер должен быть степенью двойки: 1M, 2M, 4M, 8M или 16M. Если параметр не указан, размер региона рассчитывается при инициализации кучи как 2048 частей.
Режимы сборки мусора
Параметры:
-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.
Основные этапы работы:
Параметр -XX:InitiatingHeapOccupancyPercent
определяет, когда сборщик мусора будет запущен при достижении определённого процента заполнения старого поколения.
Преимущества:
Недостатки:
Область действия: охватывает несколько поколений.
Сценарии использования: сборщик мусора 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 позволяет достичь сверхнизкого времени простоя, но более высокая сложность также сопровождается более высокими системными издержками, что в определённой степени влияет на пропускную способность. Ниже представлена сравнительная диаграмма времени простоя и системных издержек Shenandoah и различных сборщиков мусора:
Сборщик мусора | Время простоя | Системные издержки |
---|---|---|
Shenandoah | Сверхнизкое | Высокие |
G1 | Среднее | Средние |
Parallel GC | Высокое | Низкие |
OracleJDK не поддерживает Shenandoa. Если вы используете OpenJDK 12 или некоторые версии JDK с поддержкой Shenandoah, вы можете включить его с помощью следующих параметров: -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC
.
Формат журнала GC
Лучшие практики
Для разных сборщиков мусора в 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]
Причина: после определения конкретного направления можно провести более глубокий анализ. Обычно наиболее вероятными причинами проблем являются этапы FinalReference и обработка метаданных в таблице символов scrub. Чтобы найти конкретную проблему кода, необходимо использовать инструменты анализа памяти MAT или JProfiler. Перед использованием инструментов MAT и т. д. можно также использовать команду для просмотра гистограммы объектов, возможно, можно сразу определить проблему.
CMSCollector::refProcessingWork()
.Стратегия: зная причину задержки GC, легче решить проблему. Такие проблемы не будут возникать одновременно в больших масштабах, но иногда время одного STW может быть довольно долгим. Если влияние на бизнес велико, рекомендуется своевременно снизить нагрузку на трафик и принять следующие меры оптимизации:
-XX:+ParallelRefProcEnabled
для параллельной обработки ссылок.-XX:-CMSClassUnloadingEnabled
, JDK8 по умолчанию включает CMSClassUnloadingEnabled, что приводит к тому, что CMS пытается выгрузить класс на этапе CMS-Remark.Заключение
В нормальных условиях фоновый сборщик мусора CMS сталкивается с проблемами, которые в основном связаны с обработкой ссылок и метаданными. Что касается обработки ссылок, независимо от того, является ли это FinalReference, SoftReference, WeakReference, основным методом является определение момента создания снимка и последующее использование инструментов анализа памяти для анализа. В настоящее время нет хорошего способа обработки классов. В сборщике G1 также есть проблемы со ссылками, которые можно наблюдать в журнале Ref Proc, а методы обработки аналогичны CMS.
Сценарий семь: фрагментация памяти и деградация сборщика
Явление: параллельный алгоритм сборщика CMS деградирует до однопоточного последовательного алгоритма сборщика в режиме переднего плана, время STW становится очень большим, иногда достигая десятков секунд. Существует два типа однопоточных последовательных алгоритмов сборщика с деградацией CMS:
Причины: основные причины деградации сборщика CMS включают:
Стратегии: после анализа конкретной причины можно принять целенаправленные меры. Конкретная стратегия заключается в том, чтобы начать с причины и решить её.
-XX:UseCMSCompactAtFullCollection=true
, чтобы контролировать, выполнять ли сжатие пространства во время полного сбора мусора (по умолчанию включено, обратите внимание, что это полный сбор мусора, а не обычный сборщик CMS), а также -XX: CMSFullGCsBeforeCompaction=n
, чтобы контролировать количество полных сборок мусора перед выполнением сжатия.-XX:CMSInitiatingOccupancyFraction
, чтобы сборщик CMS мог запускаться раньше, обеспечивая достаточное непрерывное пространство и уменьшая использование пространства старого региона. Кроме того, необходимо использовать -XX:+UseCMSInitiatingOccupancyOnly
, иначе JVM только устанавливает значение при первом использовании, а затем автоматически регулирует его.-XX:+CMSScavengeBeforeRemark
, чтобы заранее запустить молодой сборщик мусора во время процесса, чтобы предотвратить продвижение слишком большого количества объектов позже.Заключение: в нормальных условиях сборщик CMS запускается в параллельном режиме, пауза очень короткая, влияние на бизнес невелико, но после деградации влияние будет очень большим. Рекомендуется полностью устранить проблему после обнаружения. Пока можно определить конкретные причины, связанные с фрагментацией памяти, плавающим мусором и инкрементным сбором, их всё ещё относительно легко решить. Что касается фрагментации памяти, если значение -XX:CMSFullGCsBeforeCompaction
выбрано неправильно, вы можете использовать -XX:PrintFLSStatistics
, чтобы наблюдать за уровнем фрагментации памяти, а затем установить конкретное значение. Наконец, при кодировании также следует избегать создания больших объектов, требующих непрерывного адресного пространства, таких как длинные строки, используемые для хранения вложений, сериализации или десериализации байтовых массивов, а также преждевременного продвижения.
Сценарий восемь: внешняя память кучи OOM
Явления: использование памяти продолжает расти, даже начинает использовать SWAP-память, и могут возникнуть ситуации, когда время GC резко возрастает, потоки блокируются и т. д., и через верхнюю команду обнаруживается, что размер RES процесса Java даже превышает -Xmx
. Когда происходят эти явления, можно сделать вывод, что произошла утечка внешней памяти кучи.
Причины: две основные причины утечки внешней памяти кучи в JVM:
Стратегии:
Сценарий девять: проблемы GC, вызванные JNI
JNI (Java Native Interface) — это интерфейс вызова Java-кода из нативного кода и наоборот. Существует два способа получения данных из JVM при использовании JNI: копирование данных или использование ссылок (указателей), что обеспечивает более высокую производительность.
Однако, если нативный код напрямую использует указатели в куче JVM, то сборщик мусора может привести к ошибкам в данных. Поэтому при вызове JNI необходимо запретить сборку мусора и предотвратить доступ других потоков к критическим областям JNI до тех пор, пока последний поток не покинет критическую область, после чего можно выполнить сборку мусора.
Стратегии
-XX+PrintJNIGCStalls
, чтобы получить информацию о потоках, вызывающих JNI, для дальнейшего анализа и определения проблемных вызовов JNI.Оптимизация производительности JVM
Анализ нехватки дискового пространства
На самом деле, анализ нехватки дискового пространства относится к системному уровню и уровню программного обеспечения, а не к JVM. Однако, с другой стороны, нехватка дискового пространства также может вызывать аномалии в работе JVM.
Для анализа нехватки дискового пространства выполните следующие шаги:
df -h
для проверки состояния диска./
.du -sh *
, чтобы просмотреть размер файлов в каталоге.Поиск процессов с высокой загрузкой процессора
Процесс поиска включает следующие шаги:
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!!!!!!!!");
}
}
Анализ:
top
, чтобы узнать, какой процесс занимает больше всего ресурсов процессора.top -Hp pid
, чтобы найти поток с самой высокой загрузкой процессора.printf '%x\n' tid
.jstack pid|grep xid -A 30
, чтобы вывести информацию о стеке потока.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:
Только область 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% проблем можно избежать. Именно так крупные компании уделяют внимание качеству кода и постоянно проверяют код.
Мониторинг использования памяти объектами и количества экземпляров.
Отслеживание времени и частоты использования процессора.
Из графиков мониторинга можно сделать вывод, что основная часть времени тратится на сетевые запросы, но не на конкретный бизнес-код, потребляющий процессорное время.
Начальный размер кучи
В Java куча используется для хранения активных объектов, то есть объектов, которые остаются в памяти во время выполнения приложения. После полного GC в старом поколении остаётся только небольшое количество долгоживущих объектов.
Точка начала и анализа размера кучи
Правила настройки молодого поколения
Minor GC собирает мусор из молодого и выжившего регионов. Если молодой регион слишком мал, Minor GC будет происходить чаще. Если регион слишком велик, Minor GC может занять много времени и не соответствовать требованиям к задержкам бизнес-систем. Поэтому после настройки кучи проанализируйте журналы GC, чтобы убедиться, что частота и продолжительность Minor GC соответствуют требованиям бизнеса.
На основе журналов 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 и время остановки, а затем производится настройка размера старого поколения в соответствии с установленными целями оптимизации.
Процесс оптимизации старого поколения:
Расследование исключений переполнения стека (включая виртуальный стек машины и локальный методный стек) аналогично расследованию 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 можно найти на следующем рисунке.
Как избежать взаимоблокировок?
Мы обсудили, как расследовать взаимоблокировки, теперь давайте поговорим о том, как их избежать. Из предыдущего примера мы видим, что взаимоблокировка возникает, когда оба потока удерживают ресурсы друг друга и не освобождают их, попадая в тупик. Поэтому, с точки зрения кода, чтобы избежать взаимоблокировок, основное внимание следует уделить следующим аспектам:
Расследование взаимоблокировок завершено, и это в основном касается проблем и их расследования, которое также можно считать частью оптимизации. Однако, говоря об оптимизации 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 стоит обратить внимание на следующие состояния потока:
Примечание: если постоянно появляется один и тот же 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)
Сценарий 2: Анализ проблемы с высокой загрузкой ЦП
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-адреса:
Шаг 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:
Посмотреть процессы со всеми записями:
jcmd process_id JFR.check [verbose]
Записи различаются по именам, JVM также присваивает каждой записи случайный номер.
Остановить запись:
jcmd process_id JFR.stop [options_list]
Параметры options_list:
Запуск JFR на примере
Создайте файл шаблона JFR (template.jfc). Для этого запустите jmc и выберите пункт меню Window->Flight Recording Template Manage. После подготовки файла экспортируйте его и перенесите в среду, где требуется устранить проблему.
Поскольку JFR требует коммерческого сертификата JDK, необходимо разблокировать коммерческие функции JDK. Выполните команду:
[root@localhost bin]# jcmd 12234 VM.unlock_commercial_features
12234: Commercial Features already unlocked.
Запустите 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
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++.
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.
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 после завершения работы.
Содержимое файла, созданного на предыдущем шаге, трудно понять невооружённым глазом, поэтому теперь вступает в действие 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/DevOps/огненный график.svg):

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

Разверните волну 2, чтобы увидеть, что метод getOurStackTrace занимает значительную часть процессорного времени. Это предполагает, что код часто использует исключения для получения текущего стека вызовов.
Посмотрите на код:
Действительно, мы обнаружили вторую основную причину потребления ресурсов процессора: new Exception().getStackTrace().
Разверните третью волну и увидите, что она связана с распаковкой Gzip:
Найдите соответствующий код и обнаружите, что каждый параметр запроса подвергается распаковке Gzip:
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )