JEP draft: JEP Draft: Unbiased Stack-Walk JVMTI extension API
Черновик JEP: API-расширение JVMTI для обхода стека без смещения
| Ответственный | Roman Kennke |
| Тип | Feature |
| Область | JDK |
| Статус | Draft |
| Компонент | hotspot / jvmti |
| Трудоёмкость | M |
| Длительность | M |
| Создан | 2026/03/30 13:45 |
| Обновлён | 2026/03/31 10:37 |
| Задача | 8381322 |
Аннотация
Предоставить API, с помощью которого внешние инструменты смогут запрашивать и получать трассировки стека без смещения (т. е. асинхронные).
Цели
- Предоставить API-расширение JVMTI, позволяющее внешним инструментам получать трассировку стека для заданного Java-потока.
- Трассировка стека передаётся через вызов callback-функций, которые предоставляет агент.
- API безопасно вызывать из обработчиков сигналов, чтобы инструменты профилирования могли вызывать его из сигналов профилирования, например при переполнении счётчика perf или по сигналам таймера CPU.
- API сообщает информацию о Java-фреймах, например имя метода/класса и индекс байт-кода.
- API может получать информацию о стеке, свободную от смещения к safepoint.
Что не является целью
- Сообщать о нативных фреймах пока не является целью. (Эта возможность может быть добавлена в последующем улучшении.)
- Изменение или расширение спецификации JVMTI не является целью. Возможность поставляется как расширение JVMTI.
Мотивация
Одно из больших преимуществ Java и JVM — обширная экосистема инструментов, облегчающих жизнь разработчикам. Одна из категорий таких инструментов — профилировщики. JVM поставляется со встроенными средствами профилирования (JFR), а кроме того, существуют различные внешние инструменты, как с открытым исходным кодом (например, async-profiler [0]), так и коммерческие, которые предоставляют более широкий, а иногда и более полезный набор возможностей. Одна из ключевых возможностей, которые решениям для профилирования нужны от JVM, — получение трассировок стека. Так инструменты профилирования могут точно показать пользователю, где в его коде могут находиться потенциальные узкие места производительности или различные другие проблемы (например, промахи кэша) и как выполнение программы туда пришло.
Сейчас у внешних инструментов профилирования есть несколько способов получать трассировки стека, и у всех есть недостатки, из-за которых они не являются достаточным решением:
- Существует семейство официальных API JVMTI для получения трассировок стека, а именно
GetStackTrace,GetAllStackTracesиGetThreadListStackTraces. Эти методы принципиально небезопасны для обработчиков сигналов, потому что они могут выделять память, переводят вызывающий поток из нативного состояния в состояние VM, что может приводить к блокировке, и выполняют handshake или даже приводят все потоки к safepoint и ждут завершения этого. Трассировки стека получаются только тогда, когда поток достигает safepoint, что приводит к так называемому смещению к safepoint. Смещение к safepoint — это проблема, потому что оно искажает результаты профилирования так, что они всегда указывают как на горячие участки только на safepoint (например, места вызова методов, обратные рёбра циклов и т. д.), даже когда настоящая проблема находится в другом месте. - Существует неофициальный API, который используют многие инструменты профилирования, —
AsyncGetCallTrace. Это внутренний API JVM, который не объявлен в заголовочном файле и не доступен никаким другим способом. Хотя он безопасен для обработчиков сигналов и избегает проблемы смещения к safepoint, у него есть ограничения, из-за которых он всё же недостаточен. Он обходит стек вне safepoint, но использует функции, рассчитанные на то, чтобы делать это только в safepoint, что потенциально может привести к аварийному завершению JVM. Кроме того, фреймы сообщаются в виде jmethodID. Инструменты должны собирать их в обработчике сигнала, а использовать позже, вне обработчика сигнала (чтобы избежать выделения памяти и переходов в VM при попытке разрешить jmethodID). Согласно спецификации JVMTI, это неопределённое поведение, и оно может приводить к аварийным завершениям, например когда метод/класс уже выгружен к моменту использования jmethodID. И HotSpot, и внешние инструменты изо всех сил стараются избежать этой проблемы, но могут лишь сделать её «очень маловероятной», чего недостаточно для стабильного решения для профилирования. - Некоторые инструменты профилирования подключаются напрямую к vmStructs, чтобы обходить стек собственными средствами. Это по своей природе опасно и не специфицировано и может меняться с каждым выпуском JVM. Аварийное завершение — чуть ли не лучший сценарий для такого «решения»: оно может приводить и к «тихим», более незаметным сбоям, которые могут оказаться катастрофичнее и сложнее в отладке, чем аварийные завершения.
Ниже показан пример того, что можно сделать с помощью нового API. Это флейм-граф, представляющий ~27000 выборок промахов кэша, полученных с помощью небольшого агента профилирования, работающего с одной из нагрузок бенчмарка Renaissance. Агент профилирования получает выборки, устанавливая в Linux обработчик сигнала о переполнении perf-счётчика аппаратных промахов кэша и запрашивая трассировку стека каждый раз, когда срабатывает этот сигнал.
Описание
Новый API добавляется как расширение JVMTI. Вызов этого API запрашивает трассировку стека текущего или указанного потока, которая будет передана через предоставленные callback-функции. API имеет следующую сигнатуру:
jvmtiError RequestStackTrace(jvmtiEnv* env, jthread* thread, void* ucontext, jvmtiStackTraceCallbacks callbacks, void* user_data)
Аргументы метода:
thread: Java-поток, для которого запрашивается трассировка стека. Для текущего потока принимаетсяNULL. Для потоков, отличных от текущего, трассировка стека будет смещённой.ucontext: контекст потока (например, переданный из обработчиков сигналов POSIX). ПринимаетсяNULL(например, когда контекст недоступен или при вызове из системы, которая не передаёт контекст потока). Если переданNULL, трассировка стека будет смещённой.callbacks: структура с callback-функциями; запрошенная трассировка стека будет передана вызовом этих функций.user_data: произвольные данные, переданные вызывающей стороной. Эти данные будут возвращены через callback-функции. Обычно агент профилирования может использовать их, чтобы связать трассировку стека, сообщённую JFR, с внутренними структурами данных агента.
Функция возвращает код ошибки:
JVMTI_ERROR_NOT_AVAILABLE, если функциональность недоступна (например, из-за отсутствия JFR)JVMTI_ERROR_INVALID_THREAD: переданный поток недействителенJVMTI_ERROR_NONE, если вызов выполнен успешно
После вызова API JVM вызовет предоставленные агентом callback-функции, чтобы передать трассировку стека.
jvmtiStackTraceCallbacks — это структура, содержащая callback-функции:
typedef struct {
jvmtiBeginStackTraceCallback beginStackTrace;
jvmtiEndStackTraceCallback endStackTrace;
jvmtiStackFrameCallback stackFrame;
jvmtiStackTraceFailureCallback failure;
} jvmtiStackTraceCallbacks;
beginStackTrace: callback вызывается в начале трассировки стека, до того как будут переданы какие-либо фреймы стека.endStackTrace: callback вызывается в конце трассировки стека, после того как переданы все фреймы стека. Освобождать структуру callback-функций и сами callback-функции безопасно непосредственно перед возвратом из этого callback.stackFrame: callback вызывается для передачи одного фрейма стека; фреймы стека передаются начиная с текущего выполняемого фрейма в направлении фрейма, с которого начался поток.failure: callback вызывается всякий раз, когда возникает сбой, не позволяющий передать трассировку стека.
typedef void (JNICALL *jvmtiBeginStackTraceCallback)
(jthread thread,
jboolean biased,
void* user_data);
thread: поток, трассировка стека которого снимаетсяbiased: смещена ли трассировка стека к safepointuserData: пользовательские данные, которые агент передал вRequestStackTrace
typedef void (JNICALL *jvmtiEndStackTraceCallback)
(jthread thread,
void* user_data);
thread: поток, трассировка стека которого снимаетсяuserData: пользовательские данные, которые агент передал вRequestStackTrace
typedef jint (JNICALL *jvmtiStackTraceCallback)
(jvmtiFrameType frameType,
jmethodID methodId,
jlocation location,
void* user_data);
frameType: тип фрейма (интерпретатор, JIT, встроенный или нативный).methodId: метод фрейма. Гарантированно действителен только до вызоваendStackTrace.location: позиция внутри метода.userData: пользовательские данные, которые агент передал вRequestStackTrace
typedef void (JNICALL *jvmtiStackTraceFailureCallback)
(jthread thread,
void* user_data);
thread: поток, трассировка стека которого снимаетсяuserData: пользовательские данные, которые агент передал вRequestStackTrace
Перед использованием функциональность нужно включить вызовом следующей функции:
jvmtiError EnableRequestStackTrace(jvmtiEnv* env)
Обычно она вызывается из Agent_OnLoad JVMTI, чтобы включить функциональность глобально. Однако эту функцию можно вызвать и позже.
Функциональность можно отключить вызовом следующей функции:
jvmtiError DisablesRequestStackTrace(jvmtiEnv* env)
Её можно вызвать из Agent_OnUnload JVMTI или в любой более ранний момент, чтобы отключить функциональность.
Реализация
Бо́льшая часть функциональности, необходимой для этой возможности, уже реализована в рамках JEP 509: JFR CPU-Time Profiling (Experimental (экспериментальная функция)). Механизм асинхронного обхода стека, реализованный для сэмплера процессорного времени, обобщается и повторно используется для Unbiased Stack-Walk API.
Вкратце механизм обхода стека работает так:
- Обработчик сигнала (или любой другой триггер) вызывает RequestStackTrace.
- RequestStackTrace записывает текущие PC, BCI и SP потока и помещает в очередь запрос на обход стека с этой информацией.
- Затем он взводит у потока опрос safepoint (т. е. handshake).
- Как только поток доходит до следующего опроса safepoint, он останавливается и начинает обрабатывать все запросы в очереди.
- Для каждого запроса механизм обхода стека получает PC, BCI и SP и по ним восстанавливает информацию о верхнем фрейме. Обратите внимание: нам нужно восстановить только верхний фрейм (плюс, возможно, встроенные фреймы), но никогда не фреймы ниже него, потому что возврат из метода всегда наталкивается на опрос safepoint.
- Получив верхний фрейм, поток обходит стек вниз обычными механизмами.
- При обходе фреймов вызываются предоставленные агентом callback-функции, чтобы передать фреймы.
Альтернативы
Рассматривались следующие альтернативы:
- Реализовать всю возможную функциональность в JFR. Это невозможно: 1. Слишком много разных сценариев. Например, события JFR можно было бы предоставить для разных переполнений событий perf в Linux. 2. Многие сценарии могут сильно зависеть от платформы (например, переполнения событий perf в Linux). Заметим, что нечто подобное уже было предпринято в JEP 509, и хотя это хорошо работает для своей задачи, оно покрывает только один очень частный сценарий на одной конкретной платформе.
- Изменить реализацию семейства функций JVMTI
GetStackTrace, чтобы обеспечить нужную функциональность (безопасность для сигналов и отсутствие смещения к safepoint). Хотя в принципе это может быть возможно, потребовалось бы изменить сигнатуру, чтобы она принимала такжеvoid* ucontext, и это означало бы существенное изменение поведения, что нежелательно. Кроме того, возникли бы те же проблемы, что и уAsyncGetCallTrace: вызывающей стороне пришлось бы заранее выделять память под структуру трассировки стека, а также иметь дело с некорректными (с неопределённым поведением)jmethodID. - Исправить
AsyncGetCallTrace. В нынешнем видеASGCTстрадает от различных фундаментальных проблем в своём дизайне (см. обсуждение выше). По сути, этот JEP и есть попытка его исправить, предоставив более устойчивую альтернативу. - Аналогичный новый API-расширение JVMTI, который, однако, генерировал бы событие JFR вместо вызова callback-функций JVMTI. Этот подход описан в JEP Draft (черновик): Unbiased Stack-Walk JFR event trigger
Тестирование
- Добавляется несколько новых тестов jtreg, проверяющих, что новая функциональность работает в соответствии со спецификацией
Риски и допущения
TBD
