Статьи

Собственная куча: повторяйте экземпляры классов с помощью JVMTI

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

Я провожу свои дни за исследованиями, написанием и написанием кода в команде RebelLabs в ZeroTurnaround , компании, которая создает инструменты для разработчиков Java, которые в основном работают как javaagents. Часто бывает так, что если вы хотите улучшить JVM, не переписывая его, или получить какую-то приличную мощность в JVM, вам придется погрузиться в прекрасный мир Java-агентов. Они бывают двух видов: Java-агенты и нативные. В этом посте мы сосредоточимся на последнем.


Обратите внимание, что презентация GeeCON в Праге Антона Архипова , который является руководителем продукта XRebel , является хорошей отправной точкой для изучения javaagents, полностью написанных на Java: развлекайтесь с Javassist .

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

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

Представьте, что вы заслуживающий доверия хакерский эльф Санты, и у большого красного есть следующая задача:

Санта: Мой дорогой эльф-хакер, не мог бы ты написать программу, которая укажет, сколько объектов Thread в настоящее время скрыто в куче JVM?

Другой эльф, который не любит бросать вызов самому себе, ответил бы: это легко и просто, верно?

1
return Thread.getAllStackTraces().size();

Но что, если мы хотим чрезмерно спроектировать наше решение, чтобы иметь возможность ответить на этот вопрос о любом данном классе? Скажем, мы хотим реализовать следующий интерфейс?

1
2
3
public interface HeapInsight {
  int countInstances(Class klass);
}

Да, это невозможно, верно? Что если вы получите String.class в качестве аргумента? Не бойтесь, нам просто нужно немного углубиться во внутренности JVM. Авторы библиотек JVM могут воспользоваться JVMTI , интерфейсом виртуальных машин Java. Он был добавлен давным-давно, и многие инструменты, которые кажутся волшебными, используют его. JVMTI предлагает две вещи:

  • нативный API
  • API инструментария для мониторинга и преобразования байт-кода классов, загруженных в JVM.

Для целей нашего примера нам понадобится доступ к нативному API. Мы хотим использовать функцию IterateThroughHeap , которая позволяет нам предоставлять собственный обратный вызов для выполнения для каждого объекта данного класса.

Прежде всего, давайте создадим собственный агент, который будет загружать и выводить что-то, чтобы убедиться, что наша инфраструктура работает.

Нативный агент — это что-то написанное на C / C ++ и скомпилированное в динамическую библиотеку для загрузки, прежде чем мы даже начнем думать о Java. Если вы не разбираетесь в C ++, не волнуйтесь, многие эльфы нет, и это не составит труда. Мой подход к C ++ включает в себя две основные тактики: программирование по совпадению и избежание ошибок. Поэтому, поскольку мне удалось написать и прокомментировать пример кода для этого поста, все вместе мы можем пройти через него. Примечание: параграф выше должен служить отказом от ответственности, не помещайте этот код в какую-либо ценную для вас среду.

Вот как вы создаете свой первый нативный агент:

01
02
03
04
05
06
07
08
09
10
#include
#include
  
using namespace std;
  
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *jvm, char *options, void *reserved)
{
  cout << "A message from my SuperAgent!" << endl;
  return JNI_OK;
}

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

Сохраните файл как, например, native-agent.cpp, и давайте посмотрим, что мы можем сделать с превращением в библиотеку.

Я использую OSX, поэтому я использую clang для его компиляции, чтобы избавить вас от необходимости гуглить, вот полная команда:

1
clang -shared -undefined dynamic_lookup -o agent.so -I /Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/include/ -I /Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/include/darwin native-agent.cpp

Это создает файл agent.so, который является библиотекой, готовой служить нам. Чтобы проверить это, давайте создадим фиктивный привет класс Java.

1
2
3
4
5
6
package org.shelajev;
public class Main {
   public static void main(String[] args) {
       System.out.println("Hello World!");
   }
}

Когда вы запустите его с правильным параметром -agentpath, указывающим на agent.so , вы должны увидеть следующий вывод:

1
2
3
java -agentpath:agent.so org.shelajev.Main
A message from my SuperAgent!
Hello World!

Отличная работа! Теперь у нас есть все, чтобы сделать его действительно полезным. Прежде всего нам нужен экземпляр jvmtiEnv , который доступен через JavaVM * jvm, когда мы находимся в Agent_OnLoad , но не доступен позже. Поэтому мы должны хранить его где-нибудь в глобальном масштабе. Мы делаем это, объявляя глобальную структуру для хранения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include
#include
  
using namespace std;
  
typedef struct {
 jvmtiEnv *jvmti;
} GlobalAgentData;
  
static GlobalAgentData *gdata;
  
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *jvm, char *options, void *reserved)
{
  jvmtiEnv *jvmti = NULL;
  jvmtiCapabilities capa;
  jvmtiError error;
   
  // put a jvmtiEnv instance at jvmti.
  jint result = jvm->GetEnv((void **) &jvmti, JVMTI_VERSION_1_1);
  if (result != JNI_OK) {
    printf("ERROR: Unable to access JVMTI!\n");
  }
  // add a capability to tag objects
  (void)memset(∩a, 0, sizeof(jvmtiCapabilities));
  capa.can_tag_objects = 1;
  error = (jvmti)->AddCapabilities(∩a);
  
  // store jvmti in a global data
  gdata = (GlobalAgentData*) malloc(sizeof(GlobalAgentData));
  gdata->jvmti = jvmti;
  return JNI_OK;
}

Мы также обновили код, добавив возможность помечать объекты, которые нам понадобятся для перебора кучи. Подготовка уже завершена, у нас инициализирован и доступен экземпляр JVMTI. Давайте предложим это нашему Java-коду через JNI.

JNI расшифровывается как Java Native Interface , стандартный способ включения вызовов собственного кода в приложение Java. Часть Java будет довольно простой, добавьте следующее определение метода countInstances в класс Main:

01
02
03
04
05
06
07
08
09
10
11
package org.shelajev;
 
public class Main {
   public static void main(String[] args) {
       System.out.println("Hello World!");
       int a = countInstances(Thread.class);
       System.out.println("There are " + a + " instances of " + Thread.class);
   }
 
   private static native int countInstances(Class klass);
}

Чтобы приспособить нативный метод, мы должны изменить наш нативный код агента. Я объясню это через минуту, но сейчас добавлю следующие определения функций:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
extern "C"
JNICALL jint objectCountingCallback(jlong class_tag, jlong size, jlong* tag_ptr, jint length, void* user_data)
{
 int* count = (int*) user_data;
 *count += 1;
 return JVMTI_VISIT_OBJECTS;
}
  
extern "C"
JNIEXPORT jint JNICALL Java_org_shelajev_Main_countInstances(JNIEnv *env, jclass thisClass, jclass klass)
{
 int count = 0;
   jvmtiHeapCallbacks callbacks;
(void)memset(&callbacks, 0, sizeof(callbacks));
callbacks.heap_iteration_callback = &objectCountingCallback;
 jvmtiError error = gdata->jvmti->IterateThroughHeap(0, klass, &callbacks, &count);
 return count;
}

Java_org_shelajev_Main_countInstances здесь более интересен, его имя следует соглашению, начиная с Java_, затем _ полностью разделенное имя класса _, затем имя метода из кода Java. Также не забудьте объявление JNIEXPORT , в котором говорится, что функция экспортируется в мир Java.

Внутри Java_org_shelajev_Main_countInstances мы указываем функцию objectCountingCallback в качестве обратного вызова и вызываем IterateThroughHeap с параметрами, полученными из приложения Java.

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

1
JNIEnv *env, jclass thisClass, jclass klass

для метода экземпляра они будут немного другими:

1
JNIEnv *env, jobj thisInstance, jclass klass

где thisInstance указывает на объект this вызова метода Java.

Теперь определение objectCountingCallback происходит непосредственно из документации . И тело не делает ничего, кроме увеличения int.

Boom! Все сделано! Спасибо за терпеливость. Если вы все еще читаете это, вы готовы протестировать весь код выше.

Снова скомпилируйте нативный агент и запустите класс Main . Вот что я вижу:

1
2
3
java -agentpath:agent.so org.shelajev.Main
Hello World!
There are 7 instances of class java.lang.Thread

Если я добавлю тему, то t = new Thread (); В строке к основному методу я вижу 8 экземпляров в куче. Похоже, это действительно работает. Ваш счетчик потоков почти наверняка будет другим, не волнуйтесь, это нормально, потому что он учитывает потоки учета JVM, которые выполняют компиляцию, сборку мусора и т. Д.

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

Ох, если вам интересно, он найдет для меня 2423 экземпляра String. Довольно большое число, например, для небольших приложений. Также,

1
return Thread.getAllStackTraces().size();

дает мне 5, а не 8, потому что это исключает бухгалтерские темы! Разговор о тривиальных решениях, а?

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

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

Этот подход часто используется большинством чудесных инструментов JVM, и я надеюсь, что часть магии была демистифицирована для вас.