不重启 JVM,如何替换掉已经加载的类?

Posted RockyPeng3

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了不重启 JVM,如何替换掉已经加载的类?相关的知识,希望对你有一定的参考价值。

不重启 JVM,如何替换掉已经加载的类?

场景

在遥远的希艾斯星球爪哇国塞沃城中,两名年轻的程序员正在为一件事情苦恼,程序出问题了,一时看不出问题出在哪里,于是有了以下对话:

“Debug一下吧。”

“线上机器,没开Debug端口。”

“看日志,看看请求值和返回值分别是什么?”

“那段代码没打印日志。”

“改代码,加日志,重新发布一次。”

“怀疑是线程池的问题,重启会破坏现场。”

长达几十秒的沉默之后:“据说,排查问题的最高境界,就是只通过Review代码来发现问题。”

比几十秒长几十倍的沉默之后:“我轮询了那段代码一十七遍之后,终于得出一个结论。”

“结论是?”

“我还没到达只通过Review代码就能发现问题的至高境界。”

方案

  • BTrace

  • 阿里的Arthas

  • 其他楼主未知的工具

上面的Btrace在阿里的Arthas出来前是比较流行的,但个人认为使用起来还是繁琐的。具体使用的入门教程参见:BTrace入门

在Arthas出来后,个人建议使用这个,这个相比Btrace使用起来要简单多了,而且内置了一些功能,具体的使用教程参见官网:Arthas官方教程

本文

透过现象看本质,其实无论是Btrace还是Arthas,他们的底层实现原理脱离不了jvm自带的两个api:

  1. Instrumentation API
  2. Attach API

本文就是用这两个api来具体实践一下

本文所用到的完整代码已上传到码云: instrument

模拟业务代码

public class BusinessClass 

    public void businessMethod() 
        System.out.println("businessMethod code: " + new Date());
    


    public static void main(String[] args) 
        for (int i = 0; i < 10000; i++) 
            new BusinessClass().businessMethod();
            try 
                Thread.sleep(2000);
             catch (InterruptedException e) 

            
        
    

上面的代码打成一个可执行jar包,然后执行 java -jar 跑起来 ,假设跑起来后的pid是22820,记下来就行后续会用。

Agent代码

public class InstrumentAgent 
    public static void agentmain(String agentArgs, Instrumentation inst) 
        try 
            System.out.println("Agent Main Start");

            //创建一个ClassFileTransformer对象,在这个对象的transform方法中完成真正的字节码替换
            BusinessTransformer businessTransformer = new BusinessTransformer();
            inst.addTransformer(businessTransformer, true);

            //触发BusinessClass的二进制更新,执行该代码后会执行ClassFileTransformer对象的transform方法

            inst.retransformClasses(Class.forName("com.rocky.practice.instrument.BusinessClass"));


            //移除(如果不移除,多次执行attach,那么transform方法会执行多次,没有意义)
            inst.removeTransformer(businessTransformer);
            System.out.println("Agent Main End: " + Thread.currentThread().getName());

         catch (Exception e) 
            e.printStackTrace();
        
    

上面的类需要依赖jdk lib目录下的tools.jar,所以在maven中需要添加如下依赖:

<dependency>
	<groupId>com.sun</groupId>
	<artifactId>tools</artifactId>
	<version>1.5.0</version>
	<scope>system</scope>
	<systemPath>$env.JAVA_HOME/lib/tools.jar</systemPath>
</dependency>

ClassFileTransformer代码

public class BusinessTransformer implements ClassFileTransformer 
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException 
        
		System.out.println("start transform: " + className + " " + Thread.currentThread().getName());

        //这里只重新定义BusinessClass类,采用了javassist框架来完成类的重定义,也可采用其他二进制框架,比如ASM
        if (className.equalsIgnoreCase("com/rocky/practice/instrument/BusinessClass")) 

            try 
                ClassPool classPool = ClassPool.getDefault();

                try 
                    CtClass ctClass = classPool.get(className.replaceAll("/", "."));
                    CtMethod ctMethod = ctClass.getDeclaredMethod("businessMethod");
                    if (!ctMethod.isEmpty()) 
                        ctMethod.setBody("" +
                                "" +
                                "System.out.println(\\"instrument api success\\");" +
                                "");
                    
                    return ctClass.toBytecode();
                 catch (Exception e) 
                    e.printStackTrace();
                
             catch (Throwable e) 
                e.printStackTrace();
            
        

        return classfileBuffer;
    

这段代码完成的功能:重写BusinessClass中businessMethod的方法体,并生成类的二进制,功能实现需要依赖javassist框架,所以maven中添加依赖:

<dependency>
	<groupId>javassist</groupId>
	<artifactId>javassist</artifactId>
	<version>3.12.1.GA</version>
</dependency>

上面的两个类打包成另外一个jar包,比如叫agent.jar。 打这个jar包的时候需要注意下,需要自定义manifest.mf:

Manifest-Version: 1.0
Agent-Class: com.rocky.practice.instrument.InstrumentAgent
Can-Retransform-Classes: true

这里我打jar包的时候,折腾了一下,最终通过maven的插件轻松解决指定manifest.mf的问题

<plugin>
	<artifactId>maven-assembly-plugin</artifactId>
	<configuration>
		<descriptorRefs>
			<descriptorRef>jar-with-dependencies</descriptorRef>
		</descriptorRefs>
		<archive>
		
			<!--在这里指定manifest.mf的路径-->
			<manifestFile>
				src/main/resources/META-INF/MANIFEST.MF
			</manifestFile>
			
			<manifest>
				<addClasspath>true</addClasspath>
				<classpathPrefix>lib/</classpathPrefix>
			</manifest>
		</archive>
	</configuration>
	<executions>
		<execution>
			<id>make-assembly</id>
			<phase>package</phase>
			<goals>
				<goal>single</goal>
			</goals>
		</execution>
	</executions>
</plugin>

Attach API

public class AttachMain 
    public static void main(String[] args) 
        try 
            VirtualMachine attach = VirtualMachine.attach("9044");
            attach.loadAgent("D:\\\\IdeaProjects\\\\java_practice_public\\\\instrument\\\\instrument_project\\\\target\\\\instrument_project-1.0-SNAPSHOT-jar-with-dependencies.jar");
            Thread.sleep(1000000);
            attach.detach();
            System.out.println("detached");
         catch (Exception e) 
            e.printStackTrace();
        
    

这个代码中的9044修改为上面得到的pid,下面那行代码的jar包路径改为刚生成的agent.jar的路径,然后执行。顺利的话,这个main执行后,跑bussinessclass代码的进程讲会打印出类似下面的语句:

用过arthas的同学,其中有个重新定义类的功能,相信通过本文也能了解其实现原理了。

多一嘴:idea的破解之前有个破解的agent,是不是也是同样的原理呢?

本文所用到的完整代码已上传到码云: instrument

至此,本文也就实现了标题所实现的功能,开头描述的场景也就得到解决方案,下期再见

以上是关于不重启 JVM,如何替换掉已经加载的类?的主要内容,如果未能解决你的问题,请参考以下文章

骚操作:不重启 JVM,如何替换掉已经加载的类?

骚操作:不重启 JVM,如何替换掉已经加载的类?

骚操作 | 不重启 JVM,替换掉已经加载的类,偷天换日?

厉害了!不重启JVM,替换掉已经加载的类

不重启JVM,对未添加日志的方法添加日志!

JVM的类加载机制和运行过程