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

JEP 331: Low-Overhead Heap Profiling

Профилирование кучи с низкими накладными расходами

ОтветственныйJean Christophe Beyler
ТипFeature
ОбластьJDK
СтатусClosed / Delivered
Выпуск11
Компонентhotspot / jvmti
Обсуждениеhotspot dash dev at openjdk dot java dot net
ТрудоёмкостьL
РецензентыMikael Vidstedt, Robbin Ehn, Serguei Spitsyn
ОдобренMikael Vidstedt, Vladimir Kozlov
Создан2016/12/12 21:31
Обновлён2018/09/05 19:16
Задача8171119

Аннотация

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

Цели

Предоставить способ получать от JVM информацию о выделениях памяти для объектов в куче Java, который:

  • имеет настолько низкие накладные расходы, что его можно постоянно держать включённым по умолчанию,
  • доступен через чётко определённый программный интерфейс,
  • может делать выборку по всем выделениям (т. е. не ограничен выделениями в одной определённой области кучи или выделениями, выполненными одним определённым способом),
  • может быть определён независимо от реализации (т. е. не опираясь на какой-либо конкретный алгоритм GC или реализацию VM) и
  • может давать информацию как о живых, так и о мёртвых объектах Java.

Мотивация

Пользователям очень нужно понимать, что содержится в их куче. Плохое управление кучей может приводить к таким проблемам, как исчерпание кучи и пробуксовка GC (GC thrashing). Поэтому был разработан ряд инструментов, позволяющих пользователям заглянуть внутрь кучи, например Java Flight Recorder, jmap, YourKit и VisualVM.

Одной из сведений, которых не хватает большинству существующих инструментов, является место вызова (call site) конкретных выделений. Дампы кучи и гистограммы кучи этой информации не содержат. А она может быть критически важна для отладки проблем с памятью, поскольку сообщает разработчикам точное место в их коде, где произошли конкретные (и особенно неудачные) выделения.

Сейчас есть два способа получить эту информацию из HotSpot:

  • Во-первых, можно инструментировать все выделения в приложении с помощью средства перезаписи байт-кода, например Allocation Instrumenter. Затем можно настроить инструментирование так, чтобы оно снимало трассировку стека (когда она нужна).

  • Во-вторых, можно использовать Java Flight Recorder, который снимает трассировку стека при повторном заполнении TLAB и при выделении непосредственно в старом поколении. Недостатки этого подхода: а) он привязан к конкретной реализации выделения (TLAB) и пропускает выделения, не соответствующие этой схеме; б) он не позволяет пользователю настроить интервал выборки; в) он только записывает выделения в журнал, поэтому нельзя отличить живые объекты от мёртвых.

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

Описание

Новое событие и метод JVMTI

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

void JNICALL
SampledObjectAlloc(jvmtiEnv *jvmti_env,
            JNIEnv* jni_env,
            jthread thread,
            jobject object,
            jclass object_klass,
            jlong size)

где:

  • thread — поток, выделяющий jobject,
  • object — ссылка на выбранный jobject,
  • object_klass — класс для jobject, и
  • size — размер выделения.

Новый API также включает один новый метод JVMTI:

jvmtiError  SetHeapSamplingInterval(jvmtiEnv* env, jint sampling_interval)

где sampling_interval — среднее число выделенных байт между выборками. Спецификация метода:

  • Если значение ненулевое, интервал выборки обновляется, и пользователю будет отправляться обратный вызов с новым средним интервалом выборки в sampling_interval байт
    • Например, если пользователю нужна выборка на каждый мегабайт, sampling_interval будет равно 1024 * 1024.
  • Если в метод передан ноль, механизм выборки фиксирует каждое выделение, как только новый интервал будет учтён, на что может потребоваться некоторое количество выделений

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

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

Чтобы включить эту возможность, пользователь использует обычный вызов уведомления о событиях:

jvmti->SetEventNotificationMode(jvmti, JVMTI_ENABLE, JVMTI_EVENT_SAMPLED_OBJECT_ALLOC, NULL)

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

Минимум, необходимый для включения системы событий выборки, — вызвать SetEventNotificationMode с JVMTI_ENABLE и типом события JVMTI_EVENT_SAMPLED_OBJECT_ALLOC. Чтобы изменить интервал выборки, пользователь вызывает метод SetHeapSamplingInterval.

Чтобы отключить систему,

jvmti->SetEventNotificationMode(jvmti, JVMTI_DISABLE, JVMTI_EVENT_SAMPLED_OBJECT_ALLOC, NULL)

отключает уведомления о событиях и автоматически отключает механизм выборки.

Повторный вызов механизма выборки через SetEventNotificationMode снова включит его с текущим установленным интервалом выборки (либо 512 КБ по умолчанию, либо последним значением, переданным пользователем через SetHeapSamplingInterval).

Новая возможность (capability)

Чтобы защитить новую функцию и сделать её необязательной для реализаций VM, в jvmtiCapabilities вводится новая возможность с именем can_generate_sampled_object_alloc_events.

Глобальная выборка и выборка на уровне потоков

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

Полный пример

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

jvmtiEventCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.SampledObjectAlloc = &SampledObjectAlloc;

jvmtiCapabilities caps;
memset(&caps, 0, sizeof(caps));
caps.can_generate_sampled_object_alloc_events = 1;
if (JVMTI_ERROR_NONE != (*jvmti)->AddCapabilities(jvmti, &caps)) {
  return JNI_ERR;
}

if (JVMTI_ERROR_NONE != (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE,
                                       JVMTI_EVENT_SAMPLED_OBJECT_ALLOC, NULL)) {
  return JNI_ERR;
}

if (JVMTI_ERROR_NONE !=  (*jvmti)->SetEventCallbacks(jvmti, &callbacks, sizeof(jvmtiEventCallbacks)) {
  return JNI_ERR;
}

// Set the sampler to 1MB.
if (JVMTI_ERROR_NONE !=  (*jvmti)->SetHeapSamplingInterval(jvmti, 1024 * 1024)) {
  return JNI_ERR;
}

Чтобы отключить механизм выборки (отключаются события и механизм выборки):

if (JVMTI_ERROR_NONE != (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_DISABLE,
                                       JVMTI_EVENT_SAMPLED_OBJECT_ALLOC, NULL)) {
  return JNI_ERR;
}

Чтобы снова включить механизм выборки с интервалом выборки 1024 * 1024 байт, достаточно простого вызова, включающего событие:

if (JVMTI_ERROR_NONE != (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE,
                                       JVMTI_EVENT_SAMPLED_OBJECT_ALLOC, NULL)) {
  return JNI_ERR;
}

Хранение выбранных выделений на стороне пользователя

Когда генерируется событие, обратный вызов может снять трассировку стека с помощью метода JVMTI GetStackTrace. Ссылку jobject, полученную обратным вызовом, можно также обернуть в слабую ссылку JNI, чтобы определить, когда объект был удалён при сборке мусора. Такой подход позволяет пользователю собирать данные о том, какие объекты попали в выборку, а также какие из них всё ещё считаются живыми, что может быть хорошим способом понять поведение задачи.

Например, можно сделать примерно следующее:

extern "C" JNIEXPORT void JNICALL SampledObjectAlloc(jvmtiEnv *env,
                                                     JNIEnv* jni,
                                                     jthread thread,
                                                     jobject object,
                                                     jclass klass,
                                                     jlong size) {
  jvmtiFrameInfo frames[32];
  jint frame_count;
  jvmtiError err;

  err = global_jvmti->GetStackTrace(NULL, 0, 32, frames, &frame_count);
  if (err == JVMTI_ERROR_NONE && frame_count >= 1) {
    jweak ref = jni->NewWeakGlobalRef(object);
    internal_storage.add(jni, ref, size, thread, frames, frame_count);
  }
}

где internal_storage — структура данных, которая может обрабатывать выбранные объекты, учитывать, нужно ли очищать выборки, удалённые при сборке мусора, и т. д. Внутреннее устройство этой реализации зависит от способа использования и выходит за рамки этого JEP.

Интервал выборки можно использовать как средство снижения накладных расходов профилирования. При интервале выборки 512 КБ накладные расходы должны быть настолько низкими, что пользователь вполне может оставлять систему включённой по умолчанию.

Детали реализации

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

  1. Архитектурно-зависимые изменения, связанные с изменением имени поля в структуре ThreadLocalAllocationBuffer (TLAB). Эти изменения минимальны, так как это лишь изменения имён.
  2. Структура TLAB дополнена новым указателем allocation_end в дополнение к существующему указателю end. Если выборка отключена, два указателя всегда равны, и код работает как раньше. Если выборка включена, end изменяется так, чтобы указывать на место, где запрошена следующая точка выборки. Тогда любой быстрый путь будет «считать», что в этой точке TLAB заполнен, и перейдёт на медленный путь, который описан в пункте (3).
  3. Код gc/shared/collectedHeap изменён, поскольку он используется как точка входа в медленный путь выделения. Когда TLAB считается заполненным (потому что выделение прошло указатель end), код входит в collectedHeap и пытается выделить новый TLAB. В этот момент TLAB возвращается к исходному размеру, и предпринимается попытка выделения. Если выделение успешно, код делает выборку этого выделения и затем возвращает управление. Если нет, это означает, что выделение достигло конца TLAB и нужен новый TLAB. Путь кода продолжает обычное выделение нового TLAB и определяет, требует ли это выделение выборки. Если выделение считается слишком большим для TLAB, система также делает его выборку, тем самым охватывая выборкой выделения как внутри TLAB, так и вне TLAB.
  4. Когда запрашивается выборка, на стеке в месте, безопасном для отправки информации нативному агенту, создаётся объект-сборщик. Сборщик отслеживает выбранные выделения и при уничтожении собственного фрейма отправляет агенту обратный вызов. Этот механизм гарантирует, что объект инициализирован корректно.
  5. Если агент JVMTI зарегистрировал обратный вызов для события SampledObjectAlloc, событие будет вызвано, и агент получит выбранные выделения. Пример реализации можно найти в файле libHeapMonitorTest.c, который используется для тестирования в JTreg.

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

Существует несколько альтернатив системе, представленной в этом JEP. Две из них уже были описаны во введении: Flight Recorder представляет собой интересную альтернативу. Данная реализация даёт ряд преимуществ. Во-первых, JFR не позволяет задавать размер выборки и не предоставляет обратный вызов. Далее, использование в JFR системы буферов может приводить к потере выделений при исчерпании буфера. Наконец, система событий JFR не даёт средств отслеживать объекты, удалённые при сборке мусора, а значит, её нельзя использовать для получения информации о живых объектах и объектах, удалённых при сборке мусора.

Ещё одна альтернатива — инструментирование байт-кода с помощью ASM. Его накладные расходы делают его неприемлемым и нерабочим решением.

Этот JEP добавляет новую функцию в JVMTI — важный API/фреймворк для различных инструментов разработки и мониторинга. С ней агент JVMTI может использовать API профилирования кучи с низкими накладными расходами вместе с остальной функциональностью JVMTI, что даёт инструментам большую гибкость. Например, агент сам решает, нужно ли собирать трассировку стека в каждой точке события.

Тестирование

Для этой функции в фреймворке JTreg есть 16 тестов, которые проверяют: включение и выключение при нескольких потоках, одновременное выделение памяти несколькими потоками, выполняется ли выборка данных с правильным интервалом и отражают ли собранные стеки правильную информацию о программе.

Риски и допущения

При отключённой функции нет ни потерь производительности, ни рисков. Пользователь, который не включает систему, не заметит разницы в производительности.

Однако при включённой функции возможны потери производительности и памяти. В первоначальной реализации прототипа накладные расходы были минимальными (<2 %). В ней использовался более тяжеловесный механизм, изменявший код, скомпилированный JIT. В представленной здесь окончательной версии система опирается на код TLAB, и такого ухудшения быть не должно.

Текущая оценка на бенчмарке Dacapo даёт следующие накладные расходы:

  • 0 %, когда функция отключена

  • 1 %, когда функция включена с интервалом по умолчанию 512 КБ, но в обратном вызове никаких действий не выполняется (т. е. метод SampledAllocEvent пуст, но зарегистрирован в JVM).

  • 3 % накладных расходов с обратным вызовом выборки, в котором реализовано простейшее сохранение данных (используется реализация из тестов)