背景

在 JDK 1.5时代,SUN推出了 一个可以动态改变class内容的工具,在 启动之前,增加一个 -javaagent参数
这个参数后面跟的是一个自定义的jar,jar包里面就是要执行的动态注入逻辑
然后实现自定义的premain函数(需要在 MANIFEST.MF 文件中指定启动类),就可以动态修改运行期间的 class内容,达到动态替换的效果
到1.6之后,可以根据 进程的PID,动态attach到这个进程上,然后动态修改已经加载的Class内容

这里的核心是 Instrumentation,它允许修改已经加载的 Class,这样就达到了动态替换的效果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public interface Instrumentation {
    //增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,
	//参数 canRetransform 设置是否允许重新转换。
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
    
	//在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,
	//如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,
	//后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来
	//重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
    void addTransformer(ClassFileTransformer transformer);
    
	//删除一个类转换器
    boolean removeTransformer(ClassFileTransformer transformer);
    boolean isRetransformClassesSupported();
    
	//在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
    boolean isRedefineClassesSupported();
    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);
}

而 1.6 之后,目标JVM上会启动一个等待注入的socket,等拿到目标JVM的PID后,可以将这个jar动态挂载到目标JVM上,这样就可以实现真正的动态注入了

用途

实现动态注入的几个关键技术点:

  • attach socket
  • Instrumentation,属于 JPDA(Java platform debugger architecture)体系
  • 可以生成字节码的工具,如:ASM等

通过使用JVM提供的Instrumentation API来开发出来的jar包,可以用来修改已加载到JVM中的字节码文件
这个东西能玩出很多花样来,最典型的就是 APM,以及动态 trace 这些功能
方便监控、debug排查问题等

attach

attach socket的建立流程

当 使用 jstack pid 时候,由两个线程负责完成这个任务
一个是 Signal Dispatcher 线程,这个线程负责接受一个 信号,也就是 quit 的信号
然后负责初始化 attach 的socket,如果初始化失败就直接打印在控制台上,比如 kill -3 就不会初始化

attach socket并不是 JVM启动后就创建的,是通过 Signal Dispatcher 来创建的
attach socket负责接受 UNIX 的socket

attach可以接受各种指令:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
static AttachOperationFunctionInfo funcs[] = {  
  { "agentProperties",  get_agent_properties },  
  { "datadump",         data_dump },  
#ifndef SERVICES_KERNEL  
  { "dumpheap",         dump_heap },  
#endif  // SERVICES_KERNEL  
  { "load",             JvmtiExport::load_agent_library },  
  { "properties",       get_system_properties },  
  { "threaddump",       thread_dump },  
  { "inspectheap",      heap_inspection },  
  { "setflag",          set_flag },  
  { "printflag",        print_flag },  
  { NULL,               NULL }  
}; 

其中就包含 dump线程、agent 等

JPDA体系

整个 JPDA体系分为三层,由低到高:

  • Java 虚拟机工具接口(JVMTI)
  • Java 调试协议(JDWP)
  • Java 调试接口(JDI)


正式通过这三层结构提供的能力,给我们提供了调试功能。利用JVMTI提供的能力,可以实现对JVM的多种操作,
它通过接口注册各种事件勾子,在JVM事件触发时,同时触发预定义的勾子,以实现对各个JVM事件的响应,
事件包括类文件加载、异常产生与捕获、线程启动和结束、进入和退出临界区、成员变量修改、GC开始和结束、
方法调用进入和退出、临界区竞争与等待、VM启动与退出等等。

例子

将下面这些类,打成jar

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package javaagent.agent;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

// 这个是入口类,启动入口函数是 : agentmain
public class MyBizAgentMain {
    public static void agentmain(String agentArgs, Instrumentation inst) 
	throws UnmodifiableClassException {
        System.out.println("---agent called---");
		//添加类文件转换器,第二个参数必须设置为true,表示可以重新转换类文件
        inst.addTransformer(new MyClassFileTransformer(),true);
        Class[] classes = inst.getAllLoadedClasses();
        for (int i = 0; i < classes.length; i++) {
            if ("javaagent.demo.MyBizMain".equals(classes[i].getName())) {
                System.out.println("----重新加载MyBizMain开始----");
                inst.retransformClasses(classes[i]);
                System.out.println("----重新加载MyBizMain完毕----");
                break;
            }
        }
    }
}



//自定义类文件转换器,通过ASM修改MyBizMain类字节码
public class MyClassFileTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, 
	Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        if (!"javaagent/demo/MyBizMain".equals(className)) return classfileBuffer;
        //以下为ASM常规操作,详情可以查看ASM使用相关文档
        ClassReader cr = new ClassReader(classfileBuffer);
        ClassWriter cw = new ClassWriter(cr,ClassWriter.COMPUTE_FRAMES);
        ClassVisitor cv = new MyClassVisitor(ASM7,cw);
        cr.accept(cv,ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
        return cw.toByteArray();
    }
}


//自定义ClassVisitor,修改foo方法字节码
public class MyClassVisitor extends ClassVisitor {
    public MyClassVisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
                                     String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        if ("foo".equals(name)) {
            System.out.println("----准备修改foo方法----");
            return new MyMethodVisitor(api,mv,access,name,descriptor);
        }
        return mv;
    }
}


//自定义MethodVisitor,修改字节码
public class MyMethodVisitor extends AdviceAdapter {
    protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, 
	String descriptor) {
        super(api, methodVisitor, access, name, descriptor);
    }

    @Override
    protected void onMethodEnter() {
        mv.visitLdcInsn("------我是MyBizMain的Agent-----");
		//返回
        mv.visitInsn(ARETURN);
    }
}

打包

MANIFEST.MF文件内容如下

1
2
3
4
5
6
Manifest-Version: 1.0
Agent-Class: javaagent.agent.MyBizAgentMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Build-Jdk-Spec: 1.8
Created-By: Maven Jar Plugin 3.2.0

maven的pom文件中需要增加一个打包插件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<dependency>
	<groupId>org.ow2.asm</groupId>
	<artifactId>asm</artifactId>
	<version>7.1</version>
</dependency>

<build>
	<plugins>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-jar-plugin</artifactId>
			<version>3.0.2</version>
			<configuration>
				<excludes>
					<exclude>/**/*.proto</exclude>
				</excludes>
			</configuration>
		</plugin>

		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-compiler-plugin</artifactId>
			<configuration>
				<source>1.8</source>
				<target>1.8</target>
				<encoding>UTF-8</encoding>
			</configuration>
		</plugin>

		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-jar-plugin</artifactId>
			<version>3.2.0</version>
			<configuration>
				<archive>
					<!-- 打jar的文件清单,对应META-INF/MANIFEST.MF文件 -->
					<manifestEntries>
						<!-- 主程序启动类 -->
						<Agent-Class>
							javaagent.agent.MyBizAgentMain
						</Agent-Class>
						<!-- 允许重新定义类 -->
						<Can-Redefine-Classes>true</Can-Redefine-Classes>
						<!-- 允许转换并重新加载类 -->
						<Can-Retransform-Classes>true</Can-Retransform-Classes>
					</manifestEntries>
				</archive>
			</configuration>
		</plugin>
	</plugins>
</build>

执行

执行一个测试类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package javaagent.demo;

public class MyBizMain {
    public String foo() {
        return "------我是MyBizMain-----";
    }

    public static void main(String[] args) throws InterruptedException {
        MyBizMain myBizMain = new MyBizMain();
        while (true) {
            System.out.println(myBizMain.foo());
            Thread.sleep(3000);
        }
    }
}

启动类,需要将 JDK libs目录下的 tools.jar导入工程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package javaagent.demo;

import com.sun.tools.attach.VirtualMachine;
import java.io.IOException;

public class MyAttachMain {
    public static void main(String[] args) {
        VirtualMachine vm = null;
        try {
            String pid = "MyBizMain的PID";
            String path = "MyBizAgentMain等相关类打成的jar包,具体路径";
            vm = VirtualMachine.attach(pid);//MyBizMain进程ID
            vm.loadAgent(path);//java agent jar包路径
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (vm != null) {
                try {
                    vm.detach();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

执行效果

1
2
3
4
5
6
7
8
------我是MyBizMain-----
---agent called---
----重新加载MyBizMain开始----
----准备修改foo方法----
----重新加载MyBizMain完毕----
------我是MyBizMain的Agent-----
------我是MyBizMain的Agent-----
------我是MyBizMain的Agent-----

参考

Instrumentation API
A BEGINNER’S GUIDE TO JAVA AGENTS
Java Instrumentation API开发Java Agent学习记
JVMTI Attach机制与核心源码分析
字节码增强