无需重启JVM动态增加日志实操 基于Arthas
一、使用背景
通常,本地开发环境无法访问生产环境。如果在生产环境中遇到问题,则无法使用 IDE 远程调试。更糟糕的是,在生产环境中调试是不可接受的,因为它会暂停所有线程,导致服务暂停。
开发人员可以尝试在测试环境或者预发环境中复现生产环境中的问题。但是,某些问题无法在不同的环境中轻松复现,甚至在重新启动后就消失了。
如果您正在考虑在代码中添加一些日志以帮助解决问题,您将必须经历以下阶段:测试、预发,然后生产。这种方法效率低下,更糟糕的是,该问题可能无法解决,因为一旦 JVM 重新启动,它可能无法复现,如上文所述。
Arthas 旨在解决这些问题。开发人员可以在线解决生产问题。无需 JVM 重启,无需代码更改。 Arthas 作为观察者永远不会暂停正在运行的线程。
二、相关理论
对象是以类为模板创建的,每个类的普通非静态属性都是私有的,但是行为(方法)是共有的,类加载之后,类的行为存储在方法区。我们首先修改源码(比如增加日志),然后重新编译生成字节码文件,再让jvm重新加载该字节码,这样该类创建的所有对象的对应行为也就改变了,并且已经创建的对象本身的属性和状态不会改变。重新编译字节码很简单,我们主要需要考虑的是如何让jvm在不重启的前提下重新加载字节码文件。
-
参考Java官方文档
java.lang.instrument.Instrumentation
看完文档之后,我们发现这么两个接口:redefineClasses 和 retransformClasses。一个是重新定义 class,一个是修改 class。
都是替换已经存在的 class 文件,redefineClasses 是自己提供字节码文件替换掉已存在的 class 文件,retransformClasses 是在已存在的字节码文件上修改后再替换之。
运行时直接替换类很不安全。比如把某个类的一个 field 给删除这种情况会引发异常。所以如文档中所言,instrument 存在诸多的限制,我们能做的基本上也就是简单修改方法内的一些行为,这对于打印一段日志来说完全足够。 -
那怎么得到我们需要的 class 文件呢?
一个最简单的方法,是把修改后的 Java文件重新编译一遍得到 class 文件,然后调用redefineClasses 替换。 -
但是对于没有(或者拿不到,或者不方便修改)源码的文件我们应该怎么办呢?
可以用来直接编辑字节码的框架,提供接口可以让我们方便地操作字节码文件,进行注入修改类的方法,动态创造一个新的类等等操作。其中最著名的框架应该就是 ASM 了,cglib、Spring 等框架中对于字节码的操作就建立在 ASM 之上。我们都知道,Spring 的 AOP 是基于动态代理实现的,Spring 会在运行时动态创建代理类,代理类中引用被代理类,在被代理的方法执行前后进行一些神秘的操作。 -
Spring 是怎么在运行时创建代理类的呢?动态代理的美妙之处,就在于我们不必手动为每个需要被代理的类写代理类代码,Spring 在运行时会根据需要动态地创造出一个类,这里创造的过程并非通过字符串写 Java 文件,然后编译成class 文件,然后加载。Spring 会直接“创造”一个 class 文件,然后加载,创造class 文件的工具,就是 ASM 了。到这里,我们知道了用 ASM 框架直接操作 class 文件,在类中加一段打印日志的代码,然后调用 retransformClasses 就可以了。
三、Arthas实操
参考Arthas官方文档 快速入门
教程
1. 启动测试程序math-game
curl -O https://arthas.aliyun.com/math-game.jar
java -jar math-game.jar
math-game是一个简单的程序,每隔一秒生成一个随机数,再执行质因数分解,并打印出分解结果。
2. 启动 arthas
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
选择应用 java 进程:
$ $ java -jar arthas-boot.jar
* [1]: 35542
[2]: 71560 math-game.jar
math-game进程是第 2 个,则输入 2,再输入回车/enter。Arthas 会 attach 到目标进程上,并输出日志:
【这里在原来的终端中会开启一个Arthas的子终端窗口,在其中输入Arthas的特定命令进行操作】
[INFO] Try to attach process 71560
[INFO] Attach process 71560 success.
[INFO] arthas-client connect 127.0.0.1 3658
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'
wiki: https://arthas.aliyun.com/doc
version: 3.0.5.20181127201536
pid: 71560
time: 2018-11-28 19:16:24
$
3. 查看需增加日志类的源码并动态修改【由于正在运行的是class文件,需要进行反编译】
【在Arthas子窗口中执行下述命令】
jad反编译命令
$ jad demo.MathGame
# 默认情况下反编译结果里会带有ClassLoader信息,通过--source-only选项可以只打印源代码
$ jad --source-only demo.MathGame
# 以上两个命令只是查看反编译后的源码,需要讲反编译的源代码输出到Java文件中从而手写源代码增加日志(如果已有源代码可以用源代码增加日志然后编译)
# 使用jad反编译demo.MathGame输出到服务器上的指定文件夹中的文件:/Users/allen/Desktop/test/MathGame.java
$ jad --source-only demo.MathGame > /Users/allen/Desktop/test/MathGame.java
编译命令【编译之前记得修改源码,增加打印日志的语句】
# 编译MathGame.java文件到目录下/Users/allen/Desktop/test/
$ mc /Users/allen/Desktop/test/MathGame.java -d /Users/allen/Desktop/test/
# 或者也可以重新打开一个终端窗口进入目录/Users/allen/Desktop/test/,然后执行
$ javac MathGame.java
4. 热加载进JVM
【在Arthas子窗口中执行下述命令】
$ redefine /Users/allen/Desktop/test/MathGame.class
# 执行成功之后就可以看到新加的日志打印到终端了
5. 效果展示
正在运行的math-game.jar,未新增日志
执行动态加载新的class文件到JVM:
运行中增加日志打印System.out.println(“can run !!!”);效果: