Исследование технологии динамического отслеживания Java

Java JVM
Исследование технологии динамического отслеживания Java

Введение

В далеком городе Саис, Ява, двух молодых программистов забеспокоила проблема: возникла проблема с программой, и они какое-то время не могли понять, в чем проблема, поэтому у них состоялся следующий диалог:

«Отладить это».

«Онлайн-машина, порт отладки не открыт».

"Посмотрите на лог, какие значения запросов и возвращаемые значения?"

«Этот код не распечатал журнал».

«Измените код, добавьте журнал и переиздайте его один раз».

«Подозревается, что это проблема с пулом потоков, и перезапуск уничтожит сцену».

После десятков секунд молчания: «Говорят, что высшая степень устранения неполадок — это поиск проблем только путем просмотра кода».

После молчания, которое в десятки раз превышало десятки секунд: «После того, как я просканировал этот код 17 раз, я, наконец, пришел к выводу».

— Вывод?

«Я не достиг вершины поиска проблем, просто просматривая код».

Начните с JSP

Для большинства программистов Java оно будет подвержено технике, называемую JSP (страницы Java Server). Хотя эта технология, передний задний код разделен, передняя и задняя часть логики разделены, а перед передней и задней конечной тканью отделяют сегодня, она устарела, но все еще есть интересные вещи, стоит упоминание.

В то время, когда мы как раз находились в периоде входа в Java, большая часть энергии, похоже, была сосредоточена на шоу страницы JSP:

"Эта таблица показывает неправильное количество строк"

"Оказывается проблема с циклом for, измените его, обновите страницу и попробуйте снова"

"Ну да ладно, табличное отображение нормальное, но имя авторизованного лица не получено. Проблема с Сессией?"

"Возможно, еще раз поменяю, а потом попробую обновить"

......

Когда мы изменяем код снова и снова, чтобы обновить страницу браузера и попробовать еще раз, мы не можем заметить классную вещь: после того, как мы изменим код, мы просто обновляем страницу браузера, модификация вступает в силу, а также весь процесс и не Перезапустите JVM. Согласно нашему здравому смыслу, программы Java, как правило, загружают файлы классов при запуске. Если код модифицирован как JSP, и он вступает в силу без перезапуска, то проблема в начале статьи может быть решена: добавить журнал в файл Java Чтобы распечатать код вступит в силу без перезапуска, не разрушая сцену, но и нахождение проблемы. Не удалось устоять, чтобы дать ему попробовать: изменить, компилировать, заменить классный файл. Ну, нет, недавно измененный код не вступает в силу. Так почему JSP делают это? Давайте сначала посмотрим, как работает JSP.

Когда мы открываем браузер и запрашиваем доступ к файлу JSP, весь процесс выглядит так:

JSP文件处理过程

После изменения файла JSP оно может вступить в силу со временем, поскольку веб-контейнер (Tomcat) проверит, был ли изменен запрошенный файл JSP. Если есть изменения, файл JSP анализируется и транслируется в новый класс Sevlet и загружается в JVM. Последующие запросы будут обрабатываться этим новым Servet. Здесь есть проблема: согласно механизму загрузки классов Java, в одном и том же ClassLoader не допускается повторение классов. Чтобы обойти это ограничение, веб-контейнер каждый раз создает новый экземпляр ClassLoader для загрузки только что скомпилированного класса сервлета. Последующие запросы будут обрабатываться этим новым сервлетом, таким образом реализуя переключение между старым и новым JSP.

Службы HTTP не имеют состояния, поэтому сценарии JSP в основном одноразовые.Такой подход «замены» класса путем создания нового ClassLoader работает, но для других приложений, таких как платформа Spring, даже если это делается, большая часть объекты являются синглтонами.Для объектов, которые были созданы в памяти, мы не можем изменить поведение объектов, создав новый экземпляр ClassLoader.

Я просто хочу добавить печать лога без перезапуска приложения, это так сложно?

Поведение объекта Java

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

我们都知道,对象使用两种东西来描述事物:行为和属性。 Например:

public class Person{

  private int age;

  private String name;

  public void speak(String str) {

    System.out.println(str);

 }

 public Person(int age, String name) {

    this.age = age;

    this.name = name;

 }

}

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

Person personA = new Person(43, "lixunhuan");

personA.speak("我是李寻欢");

Person personB = new Person(23, "afei");

personB.speak("我是阿飞");

personA и personB имеют свои имена и возраст, но имеют общее поведение: говорят. Представьте, если бы мы были разработчиками языка Java, как бы мы сохраняли поведение и свойства объектов?

«Это очень просто. Атрибуты следуют за объектами, и у каждого объекта есть копия. Поведение — это публичная вещь, которая извлекается и размещается в отдельном месте».

«А? Вытащите общедоступную часть, это похоже на повторное использование кода».

«Дорога проста, и многие вещи имеют одну цель».

То есть первый шаг — найти общее место для хранения поведения объекта. После недолгих поисков мы нашли это описание:

Method area is created on virtual machine startup, shared among all Java virtual machine threads and it is logically part of heap area. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors.

Поведение объектов Java (методы, функции) хранится в области методов.

«Откуда берутся данные в области методов?»

«Данные в области метода извлекаются из файла класса при загрузке класса».

«Откуда взялся файл класса?»

«Скомпилировано из Java или другого исходного кода, совместимого с JVM».

«Откуда исходный код?»

"Ерунда, конечно рукописная!"

«Отправить назад, писать от руки без проблем, компилировать без проблем, что касается загрузки... Есть ли способ загрузить уже загруженный класс? Если да, мы можем изменить байт-код, в котором находится целевой метод, а затем перезагрузить класс , так что поведение (метод) объекта в области метода изменено, и это не меняет свойства объекта, а также не влияет на состояние существующего объекта, то эта проблема может быть решена. Разве это не противоречит принципу загрузки классов JVM? В конце концов, мы не хотим менять ClassLoader».

«Молодой человек, вы можете пойти и посмотретьjava.lang.instrument.Instrumentation. "

java.lang.instrument.Instrumentation

Почитав документацию, мы нашли два интерфейса: redefineClasses и retransformClasses. Один — переопределить класс, а другой — изменить класс. Эти два похожи, см. описание reDefineClasses:

This method is used to replace the definition of a class without reference to the existing class file bytes, as one might do when recompiling from source for fix-and-continue debugging. Where the existing class file bytes are to be transformed (for example in bytecode instrumentation) retransformClasses should be used.

Оба заменяют существующий файл класса, redefineClasses заменяет существующий файл класса файлом байт-кода, предоставленным им самим, а retransformClasses предназначен для изменения существующего файла байт-кода, а затем заменяет его.

Конечно, заменять классы непосредственно во время выполнения небезопасно. Например, новый файл класса ссылается на несуществующий класс, или поле определенного класса удаляется и т. д., эти случаи вызовут исключение. Итак, как говорится в документации, инструмент имеет много ограничений:

The redefinition may change method bodies, the constant pool and attributes. The redefinition must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance. These restrictions maybe be lifted in future versions. The class file bytes are not checked, verified and installed until after the transformations have been applied, if the resultant bytes are in error this method will throw an exception.

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

Так как же нам получить нужный файл класса? Один из самых простых способов — перекомпилировать измененный файл Java, чтобы получить файл класса, а затем вызвать redefineClasses для его замены. Но что нам делать с файлами, которые не имеют (или не могут получить, или неудобно модифицировать) исходный код? Фактически, для JVM, будь то Java или Scala, исходный код любого языка, соответствующего спецификации JVM, может быть скомпилирован в файл класса. Операционный объект JVM — это файл класса, а не исходный код. Итак, в этом смысле мы можем сказать, что «JVM не зависит от языка». В этом случае, независимо от того, есть ли исходный код, по сути, нам нужно только изменить файл класса.

Манипулировать байт-кодом напрямую

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

Для людей файлы байт-кода гораздо менее читаемы, чем код Java. Тем не менее, все еще есть выдающиеся программисты, которые создали фреймворки, которые можно использовать для непосредственного редактирования байт-кода, предоставляя интерфейсы, которые позволяют нам легко манипулировать файлами байт-кода, внедрять и изменять методы класса, динамически создавать новый класс и т.д. Одним из самых известных фреймворков должен быть ASM, работа с байт-кодом в таких фреймворках, как cglib и Spring, основана на ASM.

Все мы знаем, что АОП Spring реализован на основе динамических прокси-классов Spring динамически создает прокси-классы во время выполнения, прокси-классы ссылаются на прокси-классы и выполняют некоторые загадочные операции до и после выполнения прокси-методов. Итак, как Spring создает прокси-классы во время выполнения? Прелесть динамических прокси заключается в том, что нам не нужно вручную писать код прокси-класса для каждого класса, который необходимо проксировать. Spring будет динамически создавать класс по мере необходимости во время выполнения. strings, а затем компилируется в файл класса, а затем загружается. Spring непосредственно «создает» файл класса, а затем загружает его, а инструментом для создания файла класса является ASM.

На данный момент мы знаем, что Framework ASM используется для непосредственного управления файлами класса, добавить кусок кода для печати журнала в классе, а затем вызовов ретрансляцию.

BTrace

До сих пор мы оставались на уровне теоретического описания. Итак, как это реализовать? Сначала рассмотрим несколько вопросов:

  1. Кто в нашем проекте будет выполнять действия по поиску байт-кода, изменению байт-кода и последующему преобразованию? Мы не пророки, и невозможно узнать, есть ли вероятность столкнуться с такой проблемой в начале этой статьи в будущем. Учитывая соотношение цена/производительность, мы не можем разработать фрагмент кода, предназначенный для изменения байт-кода и перезагрузки байт-кода в каждом проекте.
  2. А если JVM не локальная, а удаленная?
  3. Что, если вы даже не можете использовать ASM? Может ли он быть более общим и более «тупым».

К счастью, благодаря BTrace нам не нужно писать собственный набор таких инструментов. Что такое BTrace?BTraceОткрытый исходный код, описание проекта предельно краткое:

A safe, dynamic tracing tool for the Java platform.

Инструмент BTRACE основан на защищенном языке Java, обеспечивая динамические услуги отслеживания. BTRACE ASM ASM, API API Java, приборы, разработанные для предоставления пользователей много заметок. Положитесь на эти заметки, мы можем написать скрипт BTRACE (простой Java-код) для достижения результатов, которые мы хотим, без необходимости работать глубоко в Bytecode ASM, не может вытащить себя.

Посмотрите на простой пример, официально предоставленный BTrace: перехватывайте все методы, начинающиеся с read, во всех классах пакета java.io и выводите имя класса, имя метода и имя параметра. Когда IO-нагрузка программы относительно высока, вы можете видеть, какие классы вызываются выходной информацией, разве это не удобно?


package com.sun.btrace.samples;

import com.sun.btrace.annotations.*;
import com.sun.btrace.AnyType;
import static com.sun.btrace.BTraceUtils.*;

/**
 * This sample demonstrates regular expression
 * probe matching and getting input arguments
 * as an array - so that any overload variant
 * can be traced in "one place". This example
 * traces any "readXX" method on any class in
 * java.io package. Probed class, method and arg
 * array is printed in the action.
 */
@BTrace public class ArgArray {
    @OnMethod(
        clazz="/java\\.io\\..*/",
        method="/read.*/"
    )
    public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, AnyType[] args) {
        println(pcn);
        println(pmn);
        printArray(args);
    }
}

Давайте посмотрим на другой пример: выведите количество потоков, созданных до сих пор каждые 2 секунды.


package com.sun.btrace.samples;

import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;
import com.sun.btrace.annotations.Export;

/**
 * This sample creates a jvmstat counter and
 * increments it everytime Thread.start() is
 * called. This thread count may be accessed
 * from outside the process. The @Export annotated
 * fields are mapped to jvmstat counters. The counter
 * name is "btrace." + <className> + "." + <fieldName>
 */ 
@BTrace public class ThreadCounter {

    // create a jvmstat counter using @Export
    @Export private static long count;

    @OnMethod(
        clazz="java.lang.Thread",
        method="start"
    ) 
    public static void onnewThread(@Self Thread t) {
        // updating counter is easy. Just assign to
        // the static field!
        count++;
    }

    @OnTimer(2000) 
    public static void ontimer() {
        // we can access counter as "count" as well
        // as from jvmstat counter directly.
        println(count);
        // or equivalently ...
        println(Counters.perfLong("btrace.com.sun.btrace.samples.ThreadCounter.count"));
    }
}

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

С BTrace проблема в начале статьи решается отлично. Что касается конкретных функций BTrace и того, как написать скрипт, то в проекте BTrace на Git есть много описаний и примеров.Статьи в Интернете, знакомящие с использованием BTrace, — это пески реки Ганг, поэтому не буду повторять их здесь.

Мы понимаем принцип и располагаем полезными инструментами для его поддержки.Остальное — использовать наше творчество, просто использовать его разумно в нужной сцене.

Поскольку BTrace может решить все упомянутые выше проблемы, какова архитектура BTrace?

BTrace — это следующие модули:

  1. Сценарий BTrace: используя аннотации, определенные BTrace, мы можем легко разрабатывать сценарии по мере необходимости.
  2. Компилятор: скомпилируйте сценарии BTrace в файлы классов BTrace.
  3. Клиент: отправьте файл класса агенту.
  4. Агент: на основе Java Attach Api агент может динамически подключаться к работающей JVM, а затем открывать сервер BTrace для получения сценария BTrace, отправленного клиентом; анализировать сценарий, а затем находить класс, который необходимо изменить в соответствии с правилами. в сценарии; измените слово После кода раздела вызовите интерфейс reTransform инструмента Java, чтобы завершить изменение поведения объекта и сделать его эффективным.

Архитектура всего BTrace примерно такова:

BTrace工作流程

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

  1. Не разрешено создавать объекты
  2. Создание массива запрещено
  3. Не позволяйте
  4. Исключения перехвата не допускаются
  5. Не допускается произвольный вызов методов других объектов или классов, разрешен вызов только статических методов, предусмотренных в com.sun.btrace.BTraceUtils (некоторые средства обработки данных и вывода информации).
  6. Изменение свойств класса запрещено
  7. Переменные-члены и методы не допускаются, только существующиеstatic public voidметод
  8. Внутренние классы, вложенные классы не допускаются
  9. Синхронизированные методы и синхронизированные блоки не допускаются.
  10. петли не допускаются
  11. Не допускать произвольного наследования других классов (кроме java.lang.Object, конечно)
  12. Реализация интерфейса не допускается
  13. утверждение не допускается
  14. Объекты класса не допускаются

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

Arthas

Сценарий BTRACE имеет определенные потребления обучения. Было бы лучше, если некоторые обычно используемые функции могут быть инкапсулированы, и простые команды могут быть предоставлены непосредственно на внешний мир. Инженеры Alibaba давно думают об этом. Просто в прошлом году (сентябрь 2018 г.), Alibaba Open Worked его собственный диагностический инструмент Java -Arthas. Arthas предоставляет простые операции командной строки с мощными функциями. Технические принципы, лежащие в основе этого, примерно такие же, как упомянутые в этой статье. Документация Артаса очень обширна, если вы хотите узнать о ней больше, вы можете ткнуть еездесь.

Эта статья призвана объяснить все тонкости технологии динамического отслеживания Java, а затем понять принцип, лежащий в основе этой технологии. При желании вы, читатели, можете разработать свой собственный «Frozen Throne».

Эпилог: три вещи

А теперь попробуем «взглянуть» на эти вопросы с высоты.

Инструменты Java оставили динамическое отслеживание среды выполнения, а Attach API предоставил «выход», динамически отслеживаемый при запуске, ASM значительно облегчает «человеческую» операцию операции байт-кода Java.

Такие инструменты, как JProfiler, Jvisualvm, BTrace, Arthas, были созданы предшественниками на основе Instruments и Attach API. На основе ASM были разработаны cglib и динамические прокси, за которыми последовал широко используемый Spring AOP.

Java является статическим языком и не позволяет изменять структуры данных во время выполнения. Однако все начало меняться с появлением инструментов в Java 5 и Attach API в Java 6. Несмотря на множество ограничений, усилиями наших предшественников мы создали множество блестящих технологий и значительно повысили эффективность разработчиков программного обеспечения в обнаружении проблем.

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

Двадцать пять столетий назад Дао Дэ Цзин сказал: «Дао производит одно, одно производит два, два производят три, а три производят все вещи».

2500 лет спустя процесс разработки компьютеров, вероятно, остался прежним.

об авторе

  • Гао Ян, присоединившийся к Meituan Taxi в 2017 году, отвечает за развитие расчетной системы Meituan Taxi.