Статьи

Java против нативных агентов — мощные вещи, которые они делают

Что вы должны знать перед установкой агента и как это влияет на ваш код

При создании масштабируемого серверного приложения мы тратим немало времени на размышления о том, как мы будем отслеживать, работать и обновлять наш код в процессе производства. Новое поколение инструментов развилось, чтобы помочь разработчикам Java и Scala сделать именно это. Многие из них построены на одном из самых мощных способов интеграции внешнего кода с JVM во время выполнения — агентах Java . агенты

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

  • Профилировщики используют Java-агент для изменения кода целевой платформы для внедрения нового кода, который собирает показатели производительности. Это включает как автономные, так и размещенные сервисы, такие как NewRelic или YourKit.
  • Платформа Play V1 использует Java-агент для горячей замены классов во время выполнения.
  • JRebel поднял его на новый уровень, создав технологию, которая обеспечивает плавную горячую замену классов во время выполнения, не требуя перезапусков JVM.
  • В Takipi мы используем низкоуровневые возможности, предоставляемые JVM для собственных агентов, чтобы показать фактический исходный код и значения переменных, которые вызывают ошибки.

Что могут сделать агенты?

Как я упоминал выше, существует два типа агентов — Java и native. Хотя оба загружаются в JVM практически одинаково (с использованием специального аргумента запуска JVM), они почти полностью отличаются друг от друга тем, как они построены и для чего предназначены.

Давайте посмотрим на два —

Агенты Java

Ява

Java-агенты — это файлы .jar, которые определяют специальную статическую функцию, которая будет вызываться JVM до вызова основной функции приложения. Магическая часть входит в объект Instrumentation , который передается в качестве аргумента этой функции хост-JVM. Держась за этот объект, код агента (который ведет себя как любой код Java, загруженный загрузчиком корневого класса) может сделать действительно мощные вещи.

1
2
3
public static void premain(String agentArgs, Instrumentation inst) {
myInst = inst; //grab a reference to the inst object for use later
}

Что они делают Наиболее мощная возможность, предоставляемая агенту, — это возможность динамически переписывать содержимое метода целевого класса в классе времени выполнения (структуры полей являются неизменяемыми). Этот процесс, известный как инструментарий байтового кода, позволяет агенту по существу переписывать содержимое метода во время выполнения кода.

Некоторые примеры включают добавление вызовов к определенным методам для профилирования производительности (например, endtime — starttime) или записи значений параметров (например, URL, передаваемый сервлету). Другим примером может быть перезагрузка новой версии класса без перезапуска JVM, как это делает JRebel.

Как это сделано Чтобы агент изменил код или загруженный класс, он, по сути, запускает процесс перезагрузки класса с помощью JVM, где байт-код класса заменяется новой версией. Для этого требуется, чтобы агент мог предоставить JVM новый байт-код, который можно проверить (то есть соответствует спецификации JVM). К сожалению, генерация правильного байт-кода во время выполнения не очень проста — есть много требований и крайних случаев. Для этого агенты обычно используют библиотеку для чтения и записи байт-кода, которая позволяет им загружать байт-код существующего класса в DOM-подобную структуру, изменять его, добавляя такие вещи, как вызовы профилирования, и затем сохранять DOM обратно в необработанный байт-код. ASM — популярная библиотека для этого. Он настолько популярен, что фактически используется внутренним кодом Sun для анализа байт-кода в Java.

Нативные агенты

родной

Родные агенты совершенно разные звери. Если вы думали, что Java-агенты могут позволить вам делать классные вещи, держитесь за носки, потому что нативные агенты работают на совершенно другом уровне. Нативные агенты не написаны на Java, но в основном на C ++, и на них не распространяются правила и ограничения, в соответствии с которыми работает нормальный код Java. Мало того, они снабжены чрезвычайно мощным набором возможностей, который называется JVM Tooling Interface (JVMTI).

Что они делают Этот набор API, предоставляемый jvmti.h, по существу позволяет библиотеке C ++, динамически загружаемой JVM, получать чрезвычайно высокий уровень видимости работы JVM в реальном времени. Это охватывает широкую область областей, включая GC, блокировку, манипулирование кодом, синхронизацию, управление потоками, отладку компиляции и многое другое.

TI JVM был разработан, чтобы сделать JVM максимально прозрачной, при этом сохраняя гибкость проектирования, чтобы поставщики JVM могли предоставлять различные базовые реализации. Этот набор API очень широк и содержит буквально сотни обратных вызовов и функций в JVM. Вы можете использовать их для выполнения чрезвычайно мощных задач, которые не могут сделать Java-агенты, таких как написание собственного отладчика или создание низкоуровневых инструментов анализа ошибок в реальном времени (то, чем является Takipi ).

Например, вот обратный вызов, который JVMTI предоставляет агенту, так что всякий раз, когда исключение генерируется где-либо внутри JVM, агент будет получать местоположение байт-кода, в котором было сгенерировано исключение, поток владельца, объект исключения и если / где это будет поймано. Мощные вещи действительно.

1
2
3
4
void JNICALL ExceptionCallback(jvmtiEnv *jvmti,
JNIEnv *jni, jthread thread, jmethodID method,
jlocation location, jobject exception,
jmethodID catch_method, jlocation catch_location)

Недостаток Если все, что я описал, звучит абсолютно персиково, вы можете спросить, почему не все агенты написаны как нативные агенты? Есть несколько причин, о которых следует знать, поэтому здесь они (в произвольном порядке).

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

Переносимость Поскольку нативные агенты написаны и скомпилированы как нативные библиотеки (.so / .dll), их необходимо скомпилировать и протестировать в любом количестве операционных систем, которые вы хотите поддерживать. Если вы посмотрите на Windows, OSX и различные варианты Linux, это может привести к большой работе. Сравните это с Java-агентами, которые выполняются JVM как Java-код и, следовательно, по своей природе являются переносимыми.

Манипулирование байт-кодом. Поскольку нативные агенты обычно пишутся на C ++, это означает, что они не могут использовать проверенные и настоящие библиотеки манипулирования байт-кодом Java (такие как ASM) напрямую, не возвращаясь в JVM с использованием JNI, что действительно доставляет удовольствие.

Стабильность JVM обеспечивает строгие меры безопасности, предотвращающие выполнение кода такими вещами, которые приведут к тому, что злая ОС прекратит процесс. Нарушения доступа к памяти, которые при нормальных обстоятельствах вызовут SIGSEV и приведут к сбою процесса, получат приятное исключение NullPointerException. Поскольку нативные агенты работают на том же уровне JVM (по сравнению с Java-агентами, код которых выполняется им), любые ошибки, которые они допускают, могут потенциально завершить JVM.

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