openjdk.ruOpenJDK на русском

JEP draft: Native Profiler Hook for Unbiased Stack Traces (Experimental)

Нативный хук профилировщика для получения трассировок стека без смещения (в статусе Experimental (экспериментальная функция))

AuthorsRoman Kennke, Ron Pressler
ОтветственныйRoman Kennke
ТипFeature
ОбластьJDK
СтатусSubmitted
Компонентhotspot / jvmti
Обсуждениеserviceability dash dev at openjdk dot org
ТрудоёмкостьM
РецензентыMarkus Grönlund, Serguei Spitsyn
Создан2026/03/17 12:49
Обновлён2026/07/30 13:49
Задача8380294

Аннотация

Предоставить внешним инструментам API для получения трассировок стека Java через JFR.

Цели

  • Предоставить API-расширение JVMTI, с помощью которого внешние инструменты смогут получать трассировку стека для заданного потока Java. API можно безопасно вызывать из обработчиков сигналов, поэтому инструменты профилирования могут вызывать его из сигналов профилирования, например при переполнении счётчика perf или по сигналам таймера CPU.
  • Передавать трассировку стека в виде события JFR, когда активна запись профилирования.
  • Получать трассировки стека без смещения к safepoint (safepoint bias).

Что не является целью

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

Мотивация

Профилирование, то есть определение того, какую долю вычислительного ресурса (например, процессорного времени, памяти или пропускной способности сети) потребляют разные части программы, может определить, будет ли программа эффективной или неэффективной. JDK Flight Recorder (JFR) — инфраструктура профилирования платформы Java, но внешние инструменты имеют доступ к метрикам и событиям профилирования, которых JFR не предоставляет. Например, нативные инструменты профилирования на различных платформах могут измерять, сколько промахов кэша CPU или переключений контекста потоков ОС происходит в программе.

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

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

Поддерживаемый механизм, функция JVMTI GetStackTrace (и другие похожие функции JVMTI), снимает трассировку стека не в точный момент, а ждёт, пока поток дойдёт до определённых позиций в коде, называемых safepoint. Эта проблема, известная как safepoint bias (смещение к safepoint), искажает результаты профилирования: событие профилирования, например промах кэша, приписывается не тому коду. Кроме того, эту функцию небезопасно вызывать из обработчика сигналов.

Некоторые инструменты профилирования используют внутренние неподдерживаемые механизмы JVM, например функцию AsyncGetCallTrace или механизм vmStructs, описывающий структуру данных VM. Эти приёмы позволяют избежать смещения к safepoint и могут применяться внутри обработчиков сигналов, но они опасны, поскольку могут приводить к аварийному завершению или другому неопределённому поведению. Кроме того, они могут быть чувствительны к внутренним изменениям JVM, которые происходят часто. Безопасное прямое раскрытие внутреннего устройства JVM потребовало бы ограничить внутреннее развитие JVM.

JFR, встроенный в платформу механизм профилирования, получает трассировки стека безопасно и точно (т. е. без смещения). Кроме того, поскольку JFR — часть платформы и развивается вместе с JVM, а его архитектура разделяет снятие трассировки стека и её использование для анализа, он может значительно снизить стоимость получения трассировки стека. Такие оптимизации недоступны внешним механизмам и даже JVMTI, в котором снятие трассировки стека и её использование объединены в одну операцию.

Нативный API, способный запускать снятие трассировки стека средствами JFR, позволит нативным инструментам профилировать Java-программы, и пользователи смогут получать данные профилирования, которые сегодня JFR напрямую не предоставляет.

Следующий пример показывает, что даёт такой API. Это флейм-граф (flame graph), представляющий примерно 27 000 выборок промахов кэша CPU, полученных из Java-бенчмарка с помощью небольшого агента профилирования. Агент профилирования получает выборки обработчиком сигнала при переполнении счётчика perf для аппаратного счётчика cache-misses и запрашивает трассировку стека при каждом срабатывании этого сигнала.

Flame graph showing cache misses in a Renaissance workload

Описание

Новая функция API — это расширение JVMTI. Её вызов запрашивает трассировку стека переданного jthread (или NULL для текущего потока), которая будет выведена через JFR в событии StackTraceRequest.

Обратите внимание: доставка события с трассировкой стека не гарантируется. К тому, что запрос будет молча отброшен, могут привести различные условия, в том числе неподходящее состояние потока (например, in-VM), заполненная очередь запросов, ограничение частоты и другие.

Сигнатура функции:

jvmtiError RequestJFRStackTrace(jvmtiEnv* env, jthread* thread, void* ucontext, jlong user_data)

Аргументы:

  • thread: поток Java, для которого запрашивается трассировка стека; для текущего потока передайте NULL. Для потоков, отличных от текущего, трассировка стека может быть смещённой — см. описание ucontext. Кроме того, API безопасен для обработчиков сигналов, только когда в качестве thread передаётся NULL.
  • ucontext: контекст потока (например, переданный из обработчиков сигналов POSIX). Может быть NULL, если недоступен.
    • Если thread — текущий поток (или NULL), то передача ненулевого ucontext (к которому обработчики сигналов часто имеют доступ) может дать более точный результат, чем передача NULL. Ненулевой ucontext, не снятый вызывающей стороной функции, может привести к неопределённому поведению (включая аварийное завершение VM).
    • Если thread не является текущим потоком, то при нулевом ucontext событие будет создано через handshake с потоком, и результат будет смещён к safepoint. Если ucontext ненулевой, то поток должен быть приостановлен до вызова метода, ucontext должен быть снят, пока поток приостановлен, а поток должен оставаться приостановленным до возврата из этой функции. Если эти условия не соблюдены, возможно неопределённое поведение (включая аварийное завершение VM).
  • user_data: произвольные данные, переданные вызывающей стороной. Эти данные будут возвращены в событии JFR. Обычно агент профилирования может использовать их, чтобы связать трассировку стека, выведенную JFR, с исходным событием (например, переполнением счётчика промахов кэша), которое вызвало снятие трассировки.

Функция возвращает один из следующих кодов ошибки:

  • JVMTI_ERROR_NOT_AVAILABLE: если функциональность недоступна (например, потому что JFR отсутствует или событие StackTraceRequest не включено)
  • JVMTI_ERROR_THREAD_NOT_ALIVE: если переданный поток ещё не запущен или уже завершился.
  • JVMTI_ERROR_INVALID_THREAD: если переданный поток недействителен
  • JVMTI_ERROR_NONE: если вызов выполнен успешно

Событие JFR StackTraceRequest в статусе Experimental

Если активна запись профилирования JFR, JVM обычно создаёт событие JFR StackTraceRequest в статусе Experimental (если это событие включено). У события есть следующие поля:

  • stackTrace (StackTrace): трассировка стека
  • eventThread (Thread): поток
  • userData (jlong): пользовательские данные, переданные в RequestJFRStackTrace
  • failed (boolean): true, если по какой-либо причине получить трассировку стека не удалось
  • biased (boolean): true, если трассировка стека смещена к safepoint

Использование

Профилировщик, использующий этот API, работает так:

  1. Обработчик сигнала (или любой другой триггер) вызывает RequestJFRStackTrace с соответствующим user_data. JFR использует механизм кооперативной выборки, чтобы получить точную трассировку стека и записать её.

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

Обратите внимание: чтобы воспользоваться этой возможностью, нужно запустить запись профилирования JFR и включить событие StackTraceRequest.

Связывать собираемую внутри информацию с событием StackTraceRequest с помощью userData — обязанность внешнего инструмента профилирования. Если несколько инструментов профилирования одновременно работают с одной и той же Java-программой и оба создают одинаковые значения userData, возникнет путаница. Согласовывать потенциально конфликтующие значения userData — обязанность агентов и/или пользователя агентов.

Кроме того, JFR управляет частотой создания события StackTraceRequest с помощью обычного механизма ограничения частоты JFR. Событие включено только в конфигурации profile.jfc, с частотой 10 мс. В конфигурации default.jfc оно не включено.

Альтернатива

Вместо того чтобы вызывать событие JFR из функции JVMTI, функция JVMTI могла бы возвращать снятую трассировку стека вызывающему инструменту.

  • Немедленный возврат трассировки стека потребовал бы при каждом вызове интерпретировать стек вызовов Java, чтобы он был понятен вызывающей стороне. Поскольку JFR разделяет снятие трассировки стека и её использование, в будущем он мог бы значительно снизить стоимость операции снятия.
  • Добавление новой развитой функциональности в JVMTI разделяет работу по сопровождению функциональности профилирования в платформе между JVMTI и JFR. Большинство новых существенных возможностей профилирования мы хотели бы сосредоточить в JFR.