点击上方蓝色“程序猿DD”,选择“设为星标”
回复“资源”获取独家整理的学习资料!
来源 | 公众号「Kirito的技术分享」
本文是Arthas 系列文章的第一篇。
一般线上问题比开发环境的问题更难解决,一个主要的原因便在于开发态可以任意 debug 断点调试,而线上环境一般不允许远程调试,所以在实践中,我一般习惯用 Arthas 来定位线上的问题。
Arthas 是阿里巴巴开源的 Java 应用诊断利器
Arthas 可以完成很多骚操作,今天给大家介绍的 Arthas 诊断技巧便是 -- 热更新线上代码。在生产环境热更新代码,并不是很好的行为,可能会引发一些问题
黑屏化的操作可能会导致误操作
不符合安全生产的规范,不满足可监控、可回滚、可降级
但有时候也有一些场景可以考虑使用 Arthas 来热更,例如开发环境无法复现的问题、找到修复思路后临时验证等。
本文以 Arthas 3.1.7 版本为例,主要使用到 jad
/mc
/redefine
三个指令。
在 arthas-demo 示例中,一共有两个类,一个 HelloService 类,sayHello 方法负责不断的打印 hello world
:
public class HelloService {
public void sayHello() {
System.out.println("hello world");
}
}
HelloService 用于模拟我们日常开发的一些业务 Service,另外还有一个 Main 函数,负责启动进程,并循环调用
public class Main {
public static void main(String[] args) throws InterruptedException {
HelloService helloService = new HelloService();
while (true) {
Thread.sleep(1000);
helloService.sayHello();
}
}
}
假设这段代码运行在线上,我们希望通过 Arthas 将 hello world
的输出更改为 hello arthas
。
Arthas 修改热更的逻辑主要分为三步:
jad 命令反编译出内存中的字节码,生成 class 文件
修改代码,使用 mc 命令内存编译新的 class 文件
redefine 重新加载新的 class 文件
从而达到热更新的效果
当挂载上 Arthas 之后,执行
$ jad --source-only moe.cnkirito.arthas.demo.HelloService > /tmp/HelloService.java
将字节码文件输出到指定的位置,查看其内容,与示例中的源码内容一致:
/*
* Decompiled with CFR.
*/
package moe.cnkirito.arthas.demo;
import java.io.PrintStream;
public class HelloService {
public void sayHello() {
System.out.println("hello world");
}
}
命令中 --source-only
的含义为,只输出源码部分,如果不加这个参数,在反编译出的内容头部会携带类加载器的信息:
ClassLoader:
+-sun.misc.Launcher$AppClassLoader@18b4aac2
+-sun.misc.Launcher$ExtClassLoader@20d5ad12
Location:
/Users/xujingfeng/IdeaProjects/arthas-demo/target/classes/
在服务器上可以直接使用 vi 等编辑器对源码进行编辑。将 hello world
改为 hello arthas
,为下一步做准备。
mc 命令编译文件需要传入该类对应类加载器的 hash 值,需要先使用 sc 命令查看 HelloService 的累加器信息
$ sc -d moe.cnkirito.arthas.demo.HelloService
输出:
class-info moe.cnkirito.arthas.demo.HelloService
code-source /Users/xujingfeng/IdeaProjects/arthas-demo/target/classes/
name moe.cnkirito.arthas.demo.HelloService
isInterface false
isAnnotation false
isEnum false
isAnonymousClass false
isArray false
isLocalClass false
isMemberClass false
isPrimitive false
isSynthetic false
simple-name HelloService
modifier public
annotation
interfaces
super-class +-java.lang.Object
class-loader +-sun.misc.Launcher$AppClassLoader@18b4aac2
+-sun.misc.Launcher$ExtClassLoader@20d5ad12
classLoaderHash 18b4aac2
最后一行 classLoaderHash
即为 HelloService 的类加载器 hash 值。
Arthas 支持 grep,你也可以简化该操作为:
sc -d moe.cnkirito.arthas.demo.HelloService | grep classLoaderHash
$ mc -c 18b4aac2 /tmp/HelloService.java -d /tmp
Memory compiler output:
/tmp/moe/cnkirito/arthas/demo/HelloService.class
使用 -c
指定类加载器的 hash 值。编译完成后,/tmp 目录下会生成对应的 class 字节码文件
$ redefine /tmp/moe/cnkirito/arthas/demo/HelloService.class
hello world
hello world
hello world
hello world
hello arthas
hello arthas
hello arthas
hello arthas
热更新成功
不允许新增或者删除 field/method
会出现类似下面的提示
redefine error! java.lang.UnsupportedOperationException: class redefinition failed: attempted to change the schema (add/remove fields)
运行中的方法不会立刻生效,会在下一次进入该方法时才能生效。
很好理解,并发问题
mc 命令有可能失败
因为运行时环境和编译时环境的 JDK 可能有版本差异,mc 可能会失败。如果编译失败可以在本地编译好 .class
文件,再上传到服务器
当存在内部类时,一次会生成多个 class 文件
public class HelloService {
public void sayHello() {
Inner.test();
}
public static class Inner {
public static void test() {
System.out.println("hello inner");
}
}
}
执行 mc
$ mc -c 18b4aac2 /tmp/HelloService.java -d /tmp
Memory compiler output:
/tmp/moe/cnkirito/arthas/demo/HelloService$Inner.class
/tmp/moe/cnkirito/arthas/demo/HelloService.class
注意 redefine 时也可以同时传入多个入参
$ redefine /tmp/moe/cnkirito/arthas/demo/HelloService$Inner.class /tmp/moe/cnkirito/arthas/demo/HelloService.class
redefine success, size: 2
https://blog.csdn.net/hengyunabc/article/details/87718469
本文通过OpenWrite的Markdown转换工具发布
关注我,回复“加群”加入各种主题讨论群
专栏推荐
Spring Boot 2.x:版本关系
Spring Boot 2.x:快速入门
Spring Boot 2.x:工程结构推荐
Spring Boot 2.x:配置文件详解
Spring Boot 2.x:构建RESTful API与单元测试
Spring Boot 2.x:使用Swagger2构建强大的API文档
Spring Boot 2.x:JSR-303实现请求参数校验
Spring Boot 2.x:Swagger接口分类与各元素排序问题详解
Spring Boot 2.x:Swagger静态API文档的生成
Spring Boot 2.x:找回启动日志中的请求路径列表
Spring Boot 2.x:使用JdbcTemplate访问MySQL数据库
Spring Boot 2.x:默认数据源Hikari的配置详解
Spring Boot 2.x:使用国产数据库连接池Druid