不重启JVM动态添加日志(阿里Arthas)

一、背景

如果生产环境临时出现故障,但是现运行代码未打印定位问题所需要的日志,我们通常的做法是添加日志->重新发布->重现故障。但是这样麻烦不说,最重要的是重启节点会丢失现场,也不一定能重现问题。所以我们希望找到一个不重启应用,能够动态增加日志的方法。

二、目的与实现

对象是以类为模板创建的,每个类的普通非静态属性都是私有的,但是行为(方法)是共有的,类加载之后,类的行为存储在方法区。我们首先修改源码(比如增加日志),然后重新编译生成字节码文件,再让jvm重新加载该字节码,这样该类创建的所有对象的对应行为也就改变了,并且已经创建的对象本身的属性和状态不会改变。

重新编译字节码很简单,我们主要需要考虑的是如何让jvm在不重启的前提下重新加载字节码文件。

2.1 Agent和Instrumentation

JDK在1.5之后新增了用instrument 做动态 Instrumentation的特性,开发者能利用Instrumentation构建一个独立于应用程序的代理程序,可以监测虚拟机、在字节码文件加载之前添加自定义代理实现AOP或者注册ClassFileTransformer以修改原字节码内容等。我们可以在一个普通 Java 程序运行时,通过-javaagent参数指定一个的 jar 文件(Agent)来启动代理程序(或者配置JVM启动参数)。简单说来分为以下几个步骤:

2.1.1 编写Agent程序

Agent主程序就是编写一个 Java 类,需要包含以下两个方法中的任何一个:

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

 如果[1]和[2]同时存在,那么[2]将不会生效。该方法会在主程序main方法运行之前运行,通过Instrumentation,可以重新定义字节码内容。

2.1.2 生成jar包

将这个Agent程序打包成一个 jar 文件,并在MANIFEST.MF当中加入"Premain-Class"来指定上一步骤当中包含premain方法的Java 类。

2.1.3 运行主程序时添加Agent代理

在java命令行或者JVM启动参数配置-javaagent,比如:-javaagent:Agent.jar=param1=1¶m2=2,其中param1和param2为传入premain方法的参数,需要自己解析。

关于Agent premain,可以做很多有意义的事,比如在字节码加载之前重定义内容(依赖Instrumentation),这篇文章Java项目安全发布--Jar包(class)加解密实践就是利用的此特性,感兴趣的朋友可以研究一下。

2.2 Attach机制

前面内容介绍了premain,我们可以在主程序运行之前添加代理程序,而且只能在主程序运行之前设置,这就存在一定的局限性,如果我们启动主程序时没有指定代理程序,那么就无法使用Instrumentation。

不过好在JDK1.6之后,这种情况得到了改善。我们可以在主程序开始执行以后,再启动自己的代理程序。这就依赖于Sun提供的一套扩展 API:Attach。

Attach是sun提供的一个扩展API,可以通过它向一个JVM“附着”一个代理程序,步骤如下:

1> 获取JVM进程id

可通过JPS查看或者调用VirtualMachine.list()列举出运行的所有JVM

2> 附着到指定pid的JVM

通过VirtualMachine.attach(pid)实现(静态方法)

3> 注册代理程序

通过VirtualMachine.loadAgent(jar)实现,其中VirtualMachine代表目标JVM进程,jar即我们的代理程序

4> 解除附着

通过VirtualMachine.detach()实现,其中VirtualMachine代表目标VM进程

借助Attach机制,在运行过程中动态地设置加载代理的步骤如下:

1> 编写Agent程序

相对于前文提到的premain方法,现在有一个与之类似的agentmain方法,可以在主程序开始运行之后再运行。同样的,需要包含以下两个方法中的任何一个:

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

如果[1]和[2]同时存在,那么[2]将不会生效。agentArgs和inst都和premain中的使用一样。

2> 生成jar包

将这个Agent程序打包成一个 jar 文件,并在MANIFEST.MF当中加入"Agent-Class"来指定上一步骤当中包含premain方法的Java 类。

3> 使用Attach api将agent附着到目标JVM(代码片段):

注:要使用Attach api,需要引用com.sun.tools,该工具包在%JAVA_HOME%/lib/tools.jar

List vms = VirtualMachine.list();
VirtualMachine vm = VirtualMachine.attach(vms.get(0));
vm.loadAgent("agent.jar");
vm.detach();

2.3 实现不重启JVM动态添加日志

有了前面的介绍,我们知道可以通过Attach机制向一个正在运行的JVM附着一个agent程序,在agent的agentmain方法里就可以借助Instrumentation提供的接口(redefineClasses和retransformClasses)替换已有字节码:

public static void agentmain(String agentArgs, Instrumentation inst) 
           throws ClassNotFoundException, UnmodifiableClassException, 
           InterruptedException { 
       //MyTransformer负责重定义字节码
       inst.addTransformer(new MyTransformer(), true); 
       //TargetClass为目标类
       inst.retransformClasses(TargetClass.class); 
   } 

需要注意的是,替换字节码本身是一个不安全的动作,字节码内容不一定是由java文件编译而成,也可以是其它语言,只要符合字节码规范即可,当然也可以"手写创造"(asm、javassist等框架也比较好用)。我们重制字节码可能就没有了IDE的语法检查,如果新的字节码引用了一个不存在的类,或者调用不存在的方法,或者把一个field给删除了等等,这都会引发异常。当然,即使这带有诸多限制,但我们只做日志添加,或者做一些其他改变方法行为的操作是没有什么问题的。

三、Arthas

实际上,不论是agent的使用,还是字节码的处理,当超脱了demo练习,真正运用到生产环境中的时候,一切都会变得相对复杂。我们通常都会考虑将其整合为一个工具,对外提供更加简洁、方便、人性化的操作入口。好在如今我们不用重复造轮子,阿里开源的Arthas就是这样一个工具。

关于Arthas,实际上是一个java诊断工具,它以命令行的方式对外提供服务,功能强大。具备监控JVM运行状态、定位应用的热点生成火焰图、修改重载字节码等等功能。现在我们以Arthas为工具,实现动态修改字节码内容。

3.1 构建demo程序

首先我们需要构建一个demo程序,该程序代码很简单,一个主类,一个Print类。

1) Print类包含一个print方法,输出"hello world."

2) 主类死循环睡眠一秒调用Print#print方法

Print.java:

public class Print {
    public void print(){
        System.out.println("hello world.");
    }
}

Main.java: 

public class Main {

    public static void main(String[] args) throws Exception {
        Print print = new Print();
        for (; ; ) {
            print.print();
            Thread.sleep(1000L);
        }
    }
}

将上述代码打包成jar包运行,可以看到控制台会间隔1秒打印"hello world.":

不重启JVM动态添加日志(阿里Arthas)_第1张图片

3.2 安装Arthas

实际上就是一个jar包,直接下载即可(https://arthas.aliyun.com/arthas-boot.jar),linux/MACOS等下可使用curl下载:

curl -O https://arthas.aliyun.com/arthas-boot.jar

3.3 连接到指定JVM

启动我们的demo程序之后,再运行arthas-boot.jar(运行arthas-boot.jar的用户要和启动demo程序的用户保持一致):

java -jar arthas-boot.jar

在本机运行arthas-boot,会列出本机正在运行的Java进程,然后等待用户输入:

不重启JVM动态添加日志(阿里Arthas)_第2张图片

可以看到我们运行的demo是第二个,pid为4073。输入2,回车,就会连接到4073进程上,进入控制台:

不重启JVM动态添加日志(阿里Arthas)_第3张图片

输入help指令可以列出当前支持的指令列表和介绍:

不重启JVM动态添加日志(阿里Arthas)_第4张图片

使用jad指令反编译字节码:

不重启JVM动态添加日志(阿里Arthas)_第5张图片

Arthas支持很多指令,我们接下来只演示使用redefine指令动态修改字节码。

redefine -- 加载外部的.class文件,redefine到JVM里

3.4 动态修改字节码

修改Print#print方法,输出修改为"hello world.modified":

public class Print {
    public Print() {
    }

    public void print() {
        System.out.println("hello world.modified");
    }
}

然后使用新的Print.java重新编译,生成新的class(IDE或者命令行javac编译都可)。回到Arthas,使用redefine修改字节码,指令格式为:redefine [class]

提示redefine success表示redefine成功了,这时候回到demo控制台,发现输出已经变了:

不重启JVM动态添加日志(阿里Arthas)_第6张图片

关于Arthas,官网文档介绍非常详细,要了解更加深入的用法,请移步官方文档.

Arthas官网:https://arthas.aliyun.com/

github:https://github.com/alibaba/arthas

你可能感兴趣的:(JVM,JAVA,Arthas,动态更新字节码)