Introduction to Java JVMTI and Instrumention mechanisms

Posted by creative on Fri, 08 Nov 2019 03:49:03 +0100

Or look at the blog on my CSDN:
https://blog.csdn.net/u013332124/article/details/88367630

1. Introduction to JVMTI

JVMTI (JVM Tool Interface) is a native programming interface provided by the Java virtual machine. It is an alternative version of JVMPI (Java Virtual Machine Profiler Interface) and JVMDI (Java Virtual Machine Debug Interface).

JVMTI can be used to develop and monitor virtual machines, view the internal state of the JVM, and control the execution of JVM applications.Functions that can be implemented include, but are not limited to, debugging, monitoring, thread analysis, coverage analysis tools, and so on.

Also, it is important to note that not all JVM implementations support JVMTI.

JVMTI is just a set of interfaces. To develop JVM tools, you need to write an Agent program to use these interfaces.Agent program is actually a dynamic link library written in C/C++.How to develop an agent program for JVMTI is not detailed here.If you are interested, you can click on the link at the end of the article to view it.

After we have developed the agent program through JVMTI, we compile the program into a dynamic link library and then specify to load and run the agent at jvm startup.

-agentlib:<agent-lib-name>=<options>

The agent program then starts working when the JVM starts.

How 1.1 Agent works

After an agent is started, it runs in the same process as the JVM. Most agents work by receiving requests from the client as a server, then calling the relevant interface of the JVMTI according to the request command and returning the results.

Many java monitoring and diagnostic tools work in this form.If arthas, jinfo, brace, etc.

In addition, the well-known java debugging is also based on how this works.

1.2 Introduction to JDPA

Whether we are developing debugging, we will use debugging tools.The bottom level of all the debugging tools we use is JVMTI-based calls.JVMTI itself provides a series of interfaces for debugging programs, so we can develop a set of debugging tools by writing an agent.

Although the corresponding interfaces already exist, there is a certain amount of work to develop a complete set of debugging tools based on these interfaces.To avoid repeating wheels, sun has defined a complete and independent debugging system called JDPA.

JDPA consists of three modules:

  1. JVMTI, the underlying related debug interface call.sun provides a jdwp.dll (jdwp.so) dynamic link library, which is the agent implementation we mentioned above.
  2. JDWP (Java Debug Wire Protocol) defines the communication interaction protocol between the agent and the debugging client.
  3. JDI (Java Debug Interface) is implemented by the Java language.With this set of interfaces, we can develop our own debugging tools directly using java.

[Picture upload failed... (image-3bb125-1552119475529)]

With jdwp Agent and the interactive message protocol format known, we can develop a set of debugging tools based on these.However, it is relatively time-consuming and laborious, so the birth of JDI, JDI is a set of java APIs.This allows java programmers unfamiliar with C/C++ to develop their own debugging tools.

In addition, JDI not only helps developers format JDWP data, but also provides queue, cache and other optimization services for JDWP data transmission.

Look back at the parameters you need to take when starting JVM debug:

java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000 -jar test.jar 

jdwp.dll is a built-in agent for jvm and does not require the -agentlib described above to start the agent.Start the agent here with -Xrunjdwp.Some parameters are specified later:

  • transport=dt_socket, which means to establish a connection by listening on the socket port. Here you can also choose the dt_shmem shared memory mode, but only for windows machines, where the server and client are on one machine
  • server=y indicates that the debugging server is currently in use, =n indicates that the debugging client is currently in use.
  • suspend=n means no interruption at startup (if interrupted at startup, it is generally used to debug problems that cannot be started)
  • address=8000 indicates local listening on port 8000

2. Instrumention mechanism

Although Java provides JVMTI, the corresponding agent needs to be developed in C/C++, which is not very friendly to Java developers.Therefore, the Instrumentation mechanism has been added to the new features of Java SE 5.With Instrumentation, developers can build a Java-based Agent to monitor or manipulate the JVM, such as replacing or modifying the definitions of certain classes.

2.1 Functions supported by Instrumention

Instrumention supports features that are reflected in the java.lang.instrument.Instrumentation interface:

public interface Instrumentation {
    //Add a ClassFileTransformer
    //This ClassFileTransformer transformation is then used when the class is loaded
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    void addTransformer(ClassFileTransformer transformer);
    //Remove ClassFileTransformer
    boolean removeTransformer(ClassFileTransformer transformer);

    boolean isRetransformClassesSupported();
    //Remove some of the loaded classes from the registered ClassFileTransformer transformation
    //retransformation can modify method body, but cannot change method signature, add and delete method/class member properties
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    boolean isRedefineClassesSupported();

    //Redefine a class
    void redefineClasses(ClassDefinition... definitions)
        throws  ClassNotFoundException, UnmodifiableClassException;

    boolean isModifiableClass(Class<?> theClass);

    @SuppressWarnings("rawtypes")
    Class[] getAllLoadedClasses();

    @SuppressWarnings("rawtypes")
    Class[] getInitiatedClasses(ClassLoader loader);

    long getObjectSize(Object objectToSize);

    void appendToBootstrapClassLoaderSearch(JarFile jarfile);

    void appendToSystemClassLoaderSearch(JarFile jarfile);

    boolean isNativeMethodPrefixSupported();

    void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}

We registered a ClassFileTransformer through the addTransformer method, which will be used for subsequent class loading.For loaded classes, retransformClasses can be called to re-trigger this Transformer transformation.

ClassFileTransformer can determine if you need to modify the class definition, modify it according to your own code rules, and return it to the JVM.With this Transformer class, we can implement AOP at the virtual machine level very well.

Differences between redefineClasses and retransformClasses:

  1. transform is the process of reading and transforming a class's byte stream, which needs to be fetched and then modified.The redefineClasses, on the other hand, are simpler and rougher, requiring a new class byte stream to be given directly, then replacing the old one.
  2. Transforms can add a number of transforms, and retransformClasses can allow specified classes to be re-transformed by these transforms.

2.2 Develop an Agent based on Instrumention

Using the related classes under the java.lang.instrument package, we can develop our own Agent program.

2.2.1 Writing premain function

Write a java class that directly implements either of the following two methods without inheriting or implementing any class:

//agentArgs is a string that gets set with the jvm startup parameters
//inst is the Instrumention instance we need, passed in by the JVM.We can take this instance and do a variety of things
public static void premain(String agentArgs, Instrumentation inst);  [1]
public static void premain(String agentArgs); [2]

Where [1] has a higher priority than [2], it will be executed first, [2] will be ignored when [1] and [2] coexist.

Write a PreMain:

public class PreMain {

    public static void premain(String agentArgs, Instrumentation inst) throws ClassNotFoundException,
            UnmodifiableClassException {
        inst.addTransformer(new MyTransform());
    }
}

MyTransform is a ClassFileTransformer implementation class that we define ourselves. When this class encounters a com/yjb/Test class, it undergoes a class definition conversion.

public class MyTransform implements ClassFileTransformer {

    public static final String classNumberReturns2 = "/tmp/Test.class";

    public static byte[] getBytesFromFile(String fileName) {
        try {
            // precondition
            File file = new File(fileName);
            InputStream is = new FileInputStream(file);
            long length = file.length();
            byte[] bytes = new byte[(int) length];

            // Read in the bytes
            int offset = 0;
            int numRead = 0;
            while (offset < bytes.length
                    && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
                offset += numRead;
            }

            if (offset < bytes.length) {
                throw new IOException("Could not completely read file "
                        + file.getName());
            }
            is.close();
            return bytes;
        } catch (Exception e) {
            System.out.println("error occurs in _ClassTransformer!"
                    + e.getClass().getName());
            return null;
        }
    }

    /**
     * Parameters:
     * loader - Define the class loader to convert; null if boot loader
     * className - Fully qualify the class name in its internal form and the interface name defined in The Java Virtual Machine Specification.For example, "java/util/List".
     * classBeingRedefined - Redefined or converted class if triggered by redefinition or conversion; null if loaded
     * protectionDomain - Protected domain of the class to be defined or redefined
     * classfileBuffer - Input byte buffer in class file format (must not be modified)
     * Return:
     * A well-formed class file buffer (the result of the conversion) that returns null if the conversion is not performed.
     * Throw out:
     * IllegalClassFormatException - If the input does not represent a well-formed class file
     */
    public byte[] transform(ClassLoader l, String className, Class<?> c,
                            ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {
        System.out.println("transform class-------" + className);
        if (!className.equals("com/yjb/Test")) {
            return null;
        }
        return getBytesFromFile(targetClassPath);
    }
}

2.2.2 as jar packages

Then we make a jar package from the above two classes and add "Premain-Class" to the META-INF/MAINIFEST.MF attribute to specify the PreMain class above.

We can use the maven plug-in to automatically package and write MAINIFEST.MF:

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>single</goal>
                        </goals>
                        <phase>package</phase>

                        <configuration>
                            <descriptorRefs>
                                <descriptorRef>jar-with-dependencies</descriptorRef>
                            </descriptorRefs>
                            <archive>
                                <manifestEntries>
                                    <Premain-Class>com.yjb.PreMain</Premain-Class>
                                    <Can-Redefine-Classes>true</Can-Redefine-Classes>
                                    <Can-Retransform-Classes>true</Can-Retransform-Classes>
                                    <Specification-Title>${project.name}</Specification-Title>
                                    <Specification-Version>${project.version}</Specification-Version>
                                    <Implementation-Title>${project.name}</Implementation-Title>
                                    <Implementation-Version>${project.version}</Implementation-Version>
                                </manifestEntries>
                            </archive>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

2.2.3 Writing test classes

The agent above converts the com/yjb/Test class, so we write a Test class to test.

public class Test {

    public void print() {
        System.out.println("A");
    }
}

Compile this class first, then put Test.class under / tmp.

Modify this class later:

public class Test {

    public void print() {
        System.out.println("B");
    }
    
    public static void main(String[] args) throws InterruptedException {
        new Test().print();
    }
}

Then specify at runtime plus the JVM parameter - javaagent:/toPath/agent-jar-with-dependencies.jar to find that the Test has been converted.

2.3 How to load agent s at runtime

The agent developed above needs to be started and parameters must be set at jvm startup, but many times we want to insert an agent run midway through the program runtime.With the new features in Java 6, you can load an agent by Attach.

Here's my blog about how Attach works:

https://blog.csdn.net/u013332124/article/details/88362317

An agent startup class loaded this way needs to implement one of these two methods:

public static void agentmain (String agentArgs, Instrumentation inst); [1] 
public static void agentmain (String agentArgs);[2]

Like premain, [1] has a higher priority than [2].

The target startup class is then specified by adding "AgentMain-Class" to the META-INF/MAINIFEST.MF attribute.

We can add an AgentMain class to the agent project above

public class AgentMain {

    public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException,
            UnmodifiableClassException, InterruptedException {
        //The Transform here still uses the one defined above
        inst.addTransformer(new MyTransform(), true);
        //Since Transform was added while running, you need to retransformClasses again
        Class<?> aClass = Class.forName("com.yjb.Test");
        inst.retransformClasses(aClass);
        System.out.println("Agent Main Done");
    }
}

Or package the project as agent-jar-with-dependencies.jar.

Then write a class to attach the target process and load the agent

public class AgentMainStarter {

    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException,
            AgentInitializationException {
                //This pid fills in the target process for the specific attach
        VirtualMachine attach = VirtualMachine.attach("pid");
        attach.loadAgent("/toPath/agent-jar-with-dependencies.jar");
        attach.detach();
        System.out.println("over");
    }
}

Then modify the Test class to keep it running

public class Test {

    private void print() {
        System.out.println("1111");
    }

    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        while (true) {
            test.print();
            Thread.sleep(1000L);
        }
    }
}

After running Test for a while and then running the AgentMainStarter class, you will see that the output has become the "A" under the / tmp/Test.class that was compiled the first time.Indicates that our agent process has run successfully in the target JVM.

3. References

Introduction to Java Attach mechanism

Agent implementation based on Java Instrument

IBM: Instrumentation New Features

Differences between redefineClasses and retransformClasses in Instrumentation

JVMTI Development Documentation

Official JVMTI oracle Documentation

Introduction to JVMTI and JDPA


 

Topics: Programming Java jvm Maven Attribute