Java Agent на службе JVM


    Наверное многие слышали или сталкивались с таким параметром JVM как -javaagent, увидеть этот параметр вы могли используя Jrebel или Plumbr это могло выглядеть например так JAVA_OPTS=-javaagent:[path/to/]jrebel.jar или так -javaagent:/path-to/plumbr.jar
    Хотя javaagent появился еще в версии java 1.5, многие разработчики так никогда и не использовали возможности агентов и имеют смутное представление что это такое.
    Что же это за агент? Зачем он может нам понадобиться и как написать свой?

    Что такое javaagent

    Как я написал выше javaagent это один из параметров JVM, который позволяет указать агент который будет запущен с вашим приложением, а точнее он будет запущен еще перед запуском вашего приложения. Сам агент это отдельное приложение которое предоставляет доступ к механизму манипуляции байт-кодом (java.lang.instrument) в runtime. Это если вкратце. Официальную документацию можно почитать тут, но она довольно скудная. Ничего непонятно? Итак, давайте разбираться. Лучше всего разбираться на примерах.

    Напишем элементарный агент


    package ru.habrahabr.agent;
    
    public class Agent007 {
        public static void premain(String args) {
            System.out.println("Hello! I`m java agent");        
        }
    }
    

    Обратите внимание, агент обязательно должен реализовывать метод premain со следующей сигнатурой
    public static void premain(String args);
    или
    public static void premain(String args, Instrumentation inst);

    Класс агента должен быть упакован в jar и содержать MANIFEST.MF, с обязательным атрибутом
    PreMain-Class — указывает на класс агента с premain методом. Есть и другие атрибуты агента, но они необязательные и сейчас нам не понадобятся.

    Вот так будет выглядеть наш manifest.mf.
    Manifest-Version: 1.0
    PreMain-Class: ru.habrahabr.agent.Agent007
    
    
    не забудьте добавить перевод строки в конец файла

    Теперь упакуем все это в jar
    jar -cvfm Agent007.jar manifest.mf  ru/habrahabr/agent/Agent007.class

    И наконец класс испытатель
    package ru.habrahabr.agent;
    
    public class AgentTester {
    	public static void main(String[] args) {
    		System.out.println("Hello! I`m agent tester");
    	}
    }
    

    Запускаем AgentTester из командной строки
    java -javaagent:Agent007.jar ru.habrahabr.agent.AgentTester
    Hello! I`m java agent
    Hello! I`m agent tester
    

    Из этого примера видно что:
    • метод premain исполняется еще до вызова метода main основного приложения.
    • агент указывается с помощью параметра -javaagent:jarpath[=options]

    Давайте попробуем извлечь из агента какую-нибудь пользу


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

    Напишем AgentCounter который будет выводить имя загружаемого класс и подсчитывать кол-во загруженных классов. Так мы сможем понаблюдать за работой classloader`a.

    package ru.habrahabr.agent;
    
    import java.lang.instrument.Instrumentation;
    
    public class AgentCounter {
    	public static void premain(String agentArgument, Instrumentation instrumentation) {
    		System.out.println("Agent Counter");
    		instrumentation.addTransformer(new ClassTransformer());
    	}
    }
    

    Обратите внимание, теперь я использую другую сигнатуру метода premain. В объект instrumentation я передаю ClassTransformer который и выполняет всю работу. ClassTransformer будет срабатывать каждый раз при загрузке класса. Если вы хотите использовать свой ClassTransformer, вы должны реализовать интерфейс java.lang.instrument.ClassFileTransformer и добавить свой объект через метод Instrumentation.addTransformer

    package ru.habrahabr.agent;
    
    import java.lang.instrument.ClassFileTransformer;
    import java.security.ProtectionDomain;
    
    public class ClassTransformer implements ClassFileTransformer {
    	
    	private static int count = 0;
    	
    	@Override
    	public byte[] transform(ClassLoader loader, 
    							String className,
    							Class<?> classBeingRedefined, 
    							ProtectionDomain protectionDomain,
    							byte[] classfileBuffer) {
    		System.out.println("load class: " + className.replaceAll("/", "."));
    		System.out.println(String.format("loaded %s classes", ++count));
    		return classfileBuffer;
    	}
    }
    

    classfileBuffer — это и есть байт-код текущего класса представленный в виде массива байт, для его переопределения трансформер должен вернуть новый массив байт, в данном примере мы не меняем содержимое класса поэтому просто возвращаем тот же массив.

    Пакуем агент и трансформер в новый jar
    jar -cvfm agentCounter.jar manifest.mf  ru/habrahabr/agent/AgentCounter.class ru/habrahabr/agent/ClassTransformer.class
    

    Немного модифицируем класс тестер
    package ru.habrahabr.agent;
    
    public class AgentTester {
    	public static void main(String[] args) {
    		A a = new A();
    		B b = new B();			
    		C c = null;
    	}
    }
    
    class A {};
    class B {};
    class C {};
    

    Запускаем AgentTester c новым агентом
    java -javaagent:agentCounter.jar ru.habrahabr.agent.AgentTester
    Agent Counter
    load class: sun.launcher.LauncherHelper
    loaded 1 classes
    load class: ru.habrahabr.agent.AgentTester
    loaded 2 classes
    load class: ru.habrahabr.agent.A
    loaded 3 classes
    load class: ru.habrahabr.agent.B
    loaded 4 classes
    
    для разных версий java результаты могут отличаться

    Если запустить какое-нибудь enterprise приложение с таким агентом, можно получить довольно интересные результаты, например один из проектов после старта выдал мне следующее:
    sun.reflect.GeneratedMethodAccessor230
    loaded 33597 classes
    java.rmi.server.Unreferenced
    loaded 33598 classes
    

    Измеряем размер java объектов


    Рассмотрим еще один пример использования агентов. Напишем класс который будет возвращать размер java объектов и javaagent будет играть ключевую роль. Кто как ни JVM может знать реальный размер созданного объекта, в интерфейсе Instrumentation есть замечательный метод long getObjectSize(Object objectToSize) который возвращает размер объекта. Но как из нашего приложения получить доступ к агенту? А делать ничего особенного и не придется, javaagent автоматически добавляется в classpath и нам остается только добавить в агент поле типа Instrumentation instrumentation и инициализировать его в методе premain.

    package ru.habrahabr.agent;
    
    import java.lang.instrument.Instrumentation;
    
    public class AgentMemoryCounter {
    	
    	private static Instrumentation instrumentation;
    	
    	public static void premain(String args, Instrumentation instrumentation) {
    		AgentMemoryCounter.instrumentation = instrumentation;
    	}
    	
    	public static long getSize(Object obj) {
    		if (instrumentation == null) {
    			throw new IllegalStateException("Agent not initialised");
    		}
    		return instrumentation.getObjectSize(obj);
    	}
    }
    

    Мы получаем доступ к методу AgentMemoryCounter.getSize(obj) из класса приложения.
    package ru.habrahabr.agent;
    
    import java.math.BigDecimal;
    import java.util.ArrayList;
    import java.util.Calendar;
    
    public class AgentTester {
    	public static void main(String[] args) {
    		printObjectSize(new Object());
    		printObjectSize(new A());
    		printObjectSize(1);
    		printObjectSize("string");
    		printObjectSize(Calendar.getInstance());
    		printObjectSize(new BigDecimal("999999999999999.999"));
    		printObjectSize(new ArrayList<String>());
    		printObjectSize(new Integer[100]);
    	}
    
    	public static void printObjectSize(Object obj) {
    		System.out.println(String.format("%s, size=%s", obj.getClass()
    				.getSimpleName(), AgentMemoryCounter.getSize(obj)));
    	}
    }
    
    class A {
    	Integer id;
    	String name;
    }
    

    Результаты работы приложения могу выглядеть следующим образом
    java -javaagent:agentMemoryCounter.jar ru.habrahabr.agent.AgentTester
    Agent Counter
    Object, size=8
    A, size=16
    Integer, size=16
    String, size=24
    GregorianCalendar, size=112
    BigDecimal, size=32
    ArrayList, size=24
    Integer[], size=416
    

    Обратите внимание что метод getObjectSize() не учитывает размер вложенных объектов т.е учитывается только память затраченная на ссылку на объект.

    Заключение


    Надеюсь этот пост помог понять предназначение javaagent-ов для тех кто никогда с ними не работал, также я пытался продемонстрировать альтернативное использование javaagent-а (не для трансформации байт-кода). А для чего вы используете в своих проектах агенты? Напишите в комментариях, было бы очень интересно.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 10

      0
      Спасибо за статью, интересно. Есть вопрос: загрузка классов идет в один поток? Если нет, то ClassTransformer может возвращать неверные данные.
        +2
        Загрузка идет в один поток. Иначе не работал бы static initializer, например. В JLS написано.
          0
          А не могли бы Вы указать конкретное место в JLS, где это написано?
            0
            Почитал JLS.
            Я не прав, в JLS не указывается, что загрузка идет обязательно в один поток.

            Трансформер, из документации, срабатывает не в момент загрузки класса, а в магической инициализации класса нативным методом defineClass1, в синхронизированном по дефолту блоке loadClass.
            Получается, если в приложении больше 1 потока и больше одного кастомного класслоадера грузят классы параллельно (обычно такое происходит намеренно), может получиться, что defineClass от разных класслоадеров будут вызываться из разных потоков.
            Доступ к статическому count не защищен барьером на чтение/запись, будет гонка независимо от того, как реализованы класслоадеры.

            Вообще, конечно, без эксперимента сложно уверенно судить о потокобезопасности трансформера. Лезть глубоко в jvm охоты нет.
              0
              private static final AtomicInteger count = new AtomicInteger();
              ...
              System.out.println(... count.incrementAndGet());
              
                0
                Атомик как раз и есть барьер на чтение и запись с гарантиями. Если такого барьера у переменной нет, как у count в приведенном классе ClassTransformer, то уверенно судить о потокобезопасности нельзя.
          +2
          А ещё Java Agent можно инъектировать в уже работающий процесс — Attach API.
            0
            > AgentCounter который будет выводить имя загружаемого класс и подсчитывать кол-во загруженных классов
            А есть ли способ подсчитывать количество экземпляров данного класса

            Всегда очень интересовало как выяснить: «Кто столько создал столько строк или int[], а найти родителя этих блоков?»
            Возможно это решено уже с помощью какого-то уже существующего агента или отладчика?
              0
              Для этого есть профайлеры. Например YourKit Java Profiler. Из бесплатного, например Eclipse Memory Analyzer Tool.
                0
                Конечно есть — и работающий агент есть: java-allocation-instrumenter

              Only users with full accounts can post comments. Log in, please.