Статьи

Профилирование Java: под покровом

В какой-то момент большинству разработчиков будет поручено устранить проблему с производительностью. Если в какой-то момент вам действительно повезло, что вам заплатили, чтобы беспокоиться о производительности в целом, у вас, вероятно, есть лучший инструментарий (на рабочем столе и в голове), чем у среднего разработчика. Если, с другой стороны, такое назначение является относительно редким явлением, вы, вероятно, будете полагаться на комбинацию регистраторов, отладчиков, различных профилировщиков с открытым исходным кодом (в скольких компаниях вы работали, что приведет к платной лицензии?) и ваш старый друг System.out.println ().

Эта статья предназначена для вас, если вы заинтересованы в инструментальных хуках, предоставляемых языком программирования Java. Я не буду обсуждать доступные профилировщики, ни с открытым исходным кодом, ни как-либо еще; в Интернете много информации на эту тему. Здесь вы увидите, как легко и быстро вы можете написать свой собственный профилировщик. Профилировщики, как правило, довольно дороги в эксплуатации, хотя большинство из них имеют функции (например, выборочное оборудование, отбор проб и т. Д.), Предназначенные для минимизации их воздействия во время выполнения. Но они предоставляют общую функциональность, и если вы узнаете, как они построены, вы можете разработать компактный профилировщик, который будет точно ориентирован именно на то, что вам нужно делать.

обзор

Первое, на что стоит обратить внимание — это документы API для  java.lang.instrument . Этот пакет довольно мал, содержит всего несколько классов, но он является отправной точкой для нашего массового профилировщика Java. Этот пакет предоставляет вам две ключевые функции:

  • Механизм перехвата классов Java (в частности, их байт-код)

  • Возможность изменять байт-код во время загрузки

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

Еще одна тема, затронутая в инструментарии пакета, — это механизм запуска агента. Вы можете запустить свой профилировщик либо при запуске целевого приложения, либо после него с помощью механизма прикрепления. Как отмечается в документации к пакету, запуск агента при запуске JVM полностью указан, а запуск агента после запуска JVM зависит от реализации. Реализация агента поддержки Java HotSpot запускается после запуска JVM; Я рассмотрю оба подхода в этой статье.

Минимальный инструментальный агент

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

Для создания этого агента вам необходимо знать две вещи:

  1. Как вы его запустите?

  2. Каков механизм перехвата классов по мере их загрузки?

Ответ на первый вопрос содержится в   документации по пакету java.lang.instrument . По сути, вы:

  • Укажите одну из двух сигнатур методов в своем профилировщике:

    • public static void premain(String agentArgs, Instrumentation inst)
    •  

      public static void premain(String agentArgs)
  • Добавьте строку в файл манифеста файла .jar вашего профайлера:

  • Premain-Class: полное имя вашего класса профилировщика

  • Запустите ваше целевое (подлежащее профилированию) приложение со следующей опцией командной строки:

  • -javaagent: jarpath [= параметры ]

Теперь давайте создадим пример. Создайте новый класс  com.example.profiler.Profiler следующим образом:

package com.example.profiler;

import java.lang.instrument.Instrumentation;

public class Profiler
{

public static void premain(String agentArgs, Instrumentation inst)
{
System.out.println("premain");
}

}

Затем создайте файл manifest.mf со следующей строкой:

Premain-Class: com.example.profiler.Profiler

 

Скомпилируйте класс и создайте файл .jar, включая приведенный выше манифест. Теперь у вас есть агент. Выберите приложение Java и вставьте следующую опцию в его панель запуска:

-javaagent: path to jar file

и запустите целевое приложение из оболочки или командной строки. Вы должны увидеть одну строку

premain

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

 

Перехват байт-кода

Вы, вероятно, заметили неиспользуемый  параметр Instrumentation  из нашего   метода premain выше. Когда среда выполнения Java передает вам экземпляр  Instrumentation , вам предоставляется хук, необходимый для предоставления вашего собственного  ClassFileTransformerClassFileTransformer  — это интерфейс с единственным методом —  transform . Именно в этом   методе преобразования вы получите возможность перехватывать байт-коды класса и обрабатывать его или нет, как вы выберете.

Далее мы сохраним переданный  экземпляр Instrumentation  , создадим  ClassFileTransformer и передадим его в   экземпляр Instrumentation . Ниже приведен пример:

package com.example.profiler;


import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;


public class Profiler implements ClassFileTransformer
{

protected Instrumentation instrumentation = null;

public static void premain(String agentArgs, Instrumentation inst)
{
System.out.println("premain");
Profiler profiler = new Profiler(inst);
}

public Profiler(Instrumentation inst)
{
instrumentation = inst;
instrumentation.addTransformer(this);
}

public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException
{
System.out.println("transform(" + className + ") (" + classfileBuffer.length + " bytes)");
return null;
}

}

Обратите внимание, что на этом этапе мы просто печатаем имя и размер класса, передаваемого нашему методу преобразования . Также обратите внимание на состояние Javadocs, что метод должен возвратить  нуль,  если он решит не преобразовывать класс (возврат исходного массива classbyte тоже работает). Если я запускаю простое «Привет, мир!» Программа с этим агентом, я увижу следующий вывод:

premain
transform(HelloWorld) (1173 bytes)
Hello, world! 

Теперь трудная часть

Чтобы на самом деле код инструмента, я рекомендую вам использовать библиотеку инъекции байт-кода. Для этого примера я использую Javassist . Javassist создаст представление класса Java из массива байт-кода, и оттуда очень легко получить представления конструкторов, используя  CtClass.getConstructors () , и методы, используя  CtClass.getMethods ()CtConstructor и  CtMethod InsertBefore ()  и InsertAfter ()  методы обеспечивают очень простой путь к инструменту ваш код.  

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

 

package com.example.profiler;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.io.IOException;
import java.security.ProtectionDomain;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.ByteArrayClassPath;
import javassist.CtMethod;
import javassist.NotFoundException;

public class Profiler implements ClassFileTransformer
{

protected Instrumentation instrumentation = null;
protected ClassPool classPool;

public static void premain(String agentArgs, Instrumentation inst)
{
Profiler profiler = new Profiler(inst);
}

public Profiler(Instrumentation inst)
{
instrumentation = inst;
classPool = ClassPool.getDefault();
instrumentation.addTransformer(this);
}

public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException
{
try
{
classPool.insertClassPath(new ByteArrayClassPath(className, classfileBuffer));
CtClass cc = classPool.get(className);
CtMethod[] methods = cc.getMethods();
for (int k=0; k<methods.length; k++)
{
if (methods[k].getLongName().startsWith(className))
{
methods[k].insertBefore("System.out.println(\"Entering " + methods[k].getLongName() + "\");");
methods[k].insertAfter("System.out.println(\"Exiting " + methods[k].getLongName() + "\");");
}
}

// return the new bytecode array:
byte[] newClassfileBuffer = cc.toBytecode();
return newClassfileBuffer;
}
catch (IOException ioe)
{
System.err.println(ioe.getMessage() + " transforming class " + className + "; returning uninstrumented class");
}
catch (NotFoundException nfe)
{
System.err.println(nfe.getMessage() + " transforming class " + className + "; returning uninstrumented class");
}
catch (CannotCompileException cce)
{
System.err.println(cce.getMessage() + " transforming class " + className + "; returning uninstrumented class");
}
return null;
}

}


Проверка на имя класса обходного путь для общего вопроса — «нет тела метода» CannotCompileException  брошенной Javassist , когда метод является унаследованным методом. Мы явно отобразим эти исключения в следующем примере профилировщика.

Запуск моего «Привет, мир!» Приложение с этим профилировщиком (не забудьте включить файл Javassist jar в путь к классам) выдает следующий вывод (примечание: я перепроектировал целевое приложение, так что вывод профилировщика будет немного больше):

Entering HelloWorld.main(java.lang.String[])
Entering HelloWorld.setName(java.lang.String)
Exiting HelloWorld.setName(java.lang.String)
Entering HelloWorld.emitGreeting()
Hello, world!
Exiting HelloWorld.emitGreeting()
Exiting HelloWorld.main(java.lang.String[])

Имейте в виду, что, поскольку профилировщик загружается загрузчиком системного класса в той же JVM, что и цель, вы могли бы так же легко настроить целевой код для вызова методов в своем профилировщике. Например, вы могли бы дать вашему профилировщику   метод getInstance () и внедрить код, чтобы уведомить вашего профилировщика, когда метод вводится или завершается:

package com.example.profiler;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.io.IOException;
import java.security.ProtectionDomain;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.ByteArrayClassPath;
import javassist.CtMethod;
import javassist.NotFoundException;
public class Profiler implements ClassFileTransformer
{
protected static Profiler profilerInstance = null;
protected Instrumentation instrumentation = null;
protected ClassPool classPool;
public static void premain(String agentArgs, Instrumentation inst)
{
Profiler profiler = new Profiler(inst);
}
public static Profiler getInstance()
{
return profilerInstance;
}
public Profiler(Instrumentation inst)
{
profilerInstance = this;
instrumentation = inst;
classPool = ClassPool.getDefault();
instrumentation.addTransformer(this);
}
public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException
{
classPool.insertClassPath(new ByteArrayClassPath(className, classfileBuffer));
CtClass cc = null;
try
{
cc = classPool.get(className);
}
catch (NotFoundException nfe)
{
System.err.println("NotFoundException: " + nfe.getMessage() + "; transforming class " + className + "; returning uninstrumented class");
}
CtMethod[] methods = cc.getMethods();
for (int k=0; k<methods.length; k++)
{
try
{
methods[k].insertBefore("com.example.profiler.Profiler.getInstance().notifyMethodEntry(\"" + className + "\", \"" + methods[k].getName() + "\");");
methods[k].insertAfter("com.example.profiler.Profiler.getInstance().notifyMethodExit(\"" + className + "\", \"" + methods[k].getName() + "\");");
}
catch (CannotCompileException cce)
{
System.err.println("CannotCompileException: " + cce.getMessage() + "; instrumenting method " + methods[k].getLongName() + "; method will not be instrumented");
}
}
// return the new bytecode array:
byte[] newClassfileBuffer = null;
try
{
newClassfileBuffer = cc.toBytecode();
}
catch (IOException ioe)
{
System.err.println("IOException: " + ioe.getMessage() + "; transforming class " + className + "; returning uninstrumented class");
return null;
}
catch (CannotCompileException cce)
{
System.err.println("CannotCompileException: " + cce.getMessage() + "; transforming class " + className + "; returning uninstrumented class");
return null;
}
return newClassfileBuffer;
}
public void notifyMethodEntry(String className, String methodName)
{
System.out.println("Thread '" + Thread.currentThread().getName() + "' entering method '" + methodName + "' of class '" + className + "'");
}
public void notifyMethodExit(String className, String methodName)
{
System.out.println("Thread '" + Thread.currentThread().getName() + "' exiting method '" + methodName + "' of class '" + className + "'");
}
}

В этой итерации я разбил предложение
try-catch
на несколько предложений, чтобы мы могли изящно восстановиться после неудачного применения отдельного метода. Код, вставленный в начало и конец каждого метода, теперь использует
статический
 метод getInstance () для получения экземпляра профилировщика и вызова для него  методов notify xxx () . 





Запустил этот профилировщик против моего надуманного «Привет, мир!» приложение приводит к следующему выводу:

CannotCompileException: no method body; instrumenting method java.lang.Object.wait(long); method will not be instrumented
CannotCompileException: no method body; instrumenting method java.lang.Object.notifyAll(); method will not be instrumented
CannotCompileException: no method body; instrumenting method java.lang.Object.getClass(); method will not be instrumented
CannotCompileException: no method body; instrumenting method java.lang.Object.clone(); method will not be instrumented
CannotCompileException: no method body; instrumenting method java.lang.Object.hashCode(); method will not be instrumented
CannotCompileException: no method body; instrumenting method java.lang.Object.notify(); method will not be instrumented
Thread 'main' entering method 'main' of class 'HelloWorld'
Thread 'main' entering method 'setName' of class 'HelloWorld'
Thread 'main' exiting method 'setName' of class 'HelloWorld'
Thread 'main' entering method 'emitGreeting' of class 'HelloWorld'
Hello, world!
Thread 'main' exiting method 'emitGreeting' of class 'HelloWorld'
Thread 'main' exiting method 'main' of class 'HelloWorld'

Обратите внимание, что методы, для которых нет тела, являются методами, унаследованными классом. По этой причине я обычно включаю проверку в цикле над именами методов, чтобы я использовал только методы инструментов, определенные в классе, который я использую.

Это оно! Если вы добавите дополнительную информацию к вызову уведомления (например, получите ThreadMXBean  и получите  текущее значение времени процессора потока), вы сможете увидеть, как вы можете быть на пути к созданию функционального профилировщика. Более того, вы можете целенаправленно ориентировать его на предоставление только той информации, которая вам нужна, только на тех точках касания, которые вы хотите, с гибкостью, которую было бы очень трудно достичь с помощью профилировщика «черного ящика».

 

Присоединение к запущенному приложению

Ранее я упоминал, что эта функция, хотя и поддерживается, зависит от реализации. В этом разделе я укажу вам информацию, которую необходимо прикрепить к виртуальной машине Sun HotSpot.

Если вы использовали инструменты JDK, такие как Java VisualVM или JConsole , вы видели API присоединения на работе. Если вы собираетесь присоединиться к запущенному процессу, вам нужно получить дескриптор для него (в частности , VirtualMachineDescriptor ), который подразумевает метод для перечисления процессов (см. VirtualMachine.list () ). Это дескрипторы процесса, которые вы видите в диалоговых окнах подключения вышеупомянутых инструментов во время запуска. Лучше всего начать с изучения документации API для com.sun.tools.attach . Эту информацию можно найти в документации по инструментам для загрузки документации JDK или онлайн. Оттуда, посмотрите на документацию для com.sun.tools.attach.VirtualMachine класс — он дает план для начала. Я начну с этого, указывая на некоторые проблемы, с которыми вы можете столкнуться по пути.

Основной процесс изложен ниже:

  • Создайте открытый статический метод void agentmain () в своем агенте профилирования (если вы уже создали агент профилирования во время запуска, он может содержать ту же логику, что и ваш  метод premain () ).

  • Добавьте   атрибут Agent-Class: в файл манифеста вашего профилировщика, указав полное имя вашего класса профилировщика.

  • В вашем приложении запуска выполните следующие шаги:

    1. Получите дескриптор для целевой JVM.

    2. Получите   экземпляр VirtualMachine с помощью  статического метода VirtualMachine.attach () .

    3. Загрузите агент профилирования в целевую JVM с помощью  метода VirtualMachine.loadAgent () .

    4. Отсоединиться от целевой JVM.

В этот момент  будет вызван метод agentmain () вашего профайлера,  и вы будете в бизнесе.

Ниже приведен пример кода, который будет выполнять эти шаги:

try
{
VirtualMachine vm = VirtualMachine.attach(pid);
// load agent into target VM
vm.loadAgent(agentPath);
// detach
vm.detach();
}
catch (AgentInitializationException aie)
{
System.err.println("AgentInitializationException: " + aie.getMessage());
aie.printStackTrace();
}
catch (AgentLoadException ale)
{
System.err.println("AgentLoadException: " + ale.getMessage());
ale.printStackTrace();
}
catch (AttachNotSupportedException anse)
{
System.err.println("AttachNotSupportedException: " + anse.getMessage());
anse.printStackTrace();
}
catch (IOException ioe)
{
System.err.println("IOException: " + ioe.getMessage());
ioe.printStackTrace();
}

 
Хотя я не предоставляю здесь полнофункциональный пример профилировщика, который использует API присоединения, приведенный выше код взят из профилировщика MonkeyWrench с открытым исходным кодом (который будет обсуждаться в следующей статье). Для получения более подробной информации о возможных проблемах, которые могут возникнуть при использовании этого API (и решений!), См. Мой пост «Инструментарий запуска процессов Java с помощью API com.sun.tools.attach » по адресу  http://wayne-adams.blogspot.com. /2009/07/instrumenting-running-java-processes.html .

Последние мысли

Теперь, когда у вас есть базовая структура профилировщика, вот некоторые замечания и идеи.

As I alluded to earlier, you can get a reference to the ThreadMXBean on your method entry and exit notifications. Since you can get the ID of the current thread, you can query the MXBean and retrieve its ThreadInfo object and other properties. This will give you the thread’s accumulated CPU time and the stack trace, among other things. If you want to profile CPU time, you should keep in mind that you’ll need the full method or constructor signature (parameters included), class name, and thread ID to uniquely identify the trace of execution in and out of a method. Also, to correctly handle recursive method calls, when you match an exit notification with an entry notification, you’ll have to unroll the pending method entry notifications by most-recent-entry first, in order to match the correct entry with the exit.

The stack trace information is useful because it allows you to build profiles of the paths used to reach the method or constructor you’re profiling. These profiles are called «sites» in hprof, the very useful but rather expensive profiling agent provided with the JDK. Your profiler can maintain a list of unique stack traces for each method invocation and provide a summary of the sites and their relative frequency as execution progresses.

If you’re profiling constructor activity and are interested in the sizes of objects being instantiated, the Instrumentation class has a getObjectSize() method. The API docs say that this size is implementation-dependent and an approximation. The size appears to be a shallow-copy object size.

This concludes our survey of the instrumentation API provided by the JDK. While a compact package, java.lang.instrument, in conjunction with a good bytecode-injection library, provides everything you need to get started writing a profiler of your own. Even if you don’t find yourself in the position of needing to write a profiler, knowing a little about how to do so will no doubt make you appreciate Java VisualVM or hprof — or your licensed commercial profiler — a little more than you did before!