本文主要是介绍热更新机制与基于 agent 热更新的相关实践。
1. 常见热更新方案
热更方式 | 方法体更新 | 方法签名更新 | 增删method | 增删field |
---|---|---|---|---|
Jsp/Groovy脚本热更 | o(需要埋点支持) | x | x | x |
JRebel热更 | √ | √ | √ | √ |
Agent热更 | √ | x | x | x |
1.1 Jsp/Groovy脚本热更
Jsp/Groovy只“支持”方法体更新,并且这种支持是不完全的,必须把需要动态支持的方法体埋点,热更的时候借助 JVM ClassLoader + ClassName 的唯一标识一个类的方式重新reload class,在埋点的地方用这个新加载的类替换。
这种方式实现简单,但是需要预先埋点,无法做到全局方法体热更新。并且通过reload class的方式会产生新的Class对象。
1.2 JRebel热更
JRebel的实现其实也用应用到了Agent,通过使用Agent#transform的方式,在加载字节码的时候回调JRebel,实时翻译出字节码,用以在JVM虚拟机和原来的可执行字节码中构造一个中间层,用这个中间层来支持热更新。这种实现方式完美诠释了这句名言:
Any problem in computer science can be solved by anther layer of indirection.
计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。
但是,JRebel是付费软件,并且在生产环境使用会严重影响性能。所以这种方式只适合在开发环境使用,并不是一种普遍的方式。
1.3 Agent 方式
Agent 的方式热更新是JVM 原生支持的,通过VirtualMachine#attach 以IPC 的通讯方式通知目标进程加载Agent.jar 包,目标进程回调agentmain(String, Instrumentation)方法,这个Instrumentation有一个redefineClasses() 方法:
Redefine the supplied set of classes using the supplied class files.
使用给出的字节码重新定义对应的Class。
这种方式可以做到全局热更新方法体,并且侵扰小(redefine的时候会有STW 的时间),无副作用,不会影响程序性能。所以我们的生产环境就是采用这种方式热更新。
2.项目线上热更方式实现
直接用一个新的进程A(低版本JDK需要加启动参数-bootclasspath/a:/${JAVA_HOME}/lib/tools.jar)去attach 目标进程,在Agent里执行对应的热更逻辑,这种方式下可以完全无代码侵入和其他依赖的前提下实现对目标进程的代码热更新。
3. Agent的其他应用场景
利用agent 无侵入attach目标进程、字节码载入回调,重新定义字节码这几个特性,可以有很多非常有用的Hack应用。
3.1 启动参数加入agent复用通用逻辑
启动参数加载agent,可以复用一些通用的逻辑。
3.1.1 字节码解密
借助ClassFileTransformer#transform()加载字节码回调,解密字节码。
3.1.3 复用通用性强的公共服务
通过agent埋点的方式,把公共服务无侵入附加到每一个项目,避免重复性编码,包括如下:
- 监控服务
3.2 无代码侵入下修改目标进程的行为
利用agent 无侵入attach目标进程的方式,可以做许多在进程启动前”忘了“做的事情。
3.2.1 调起jmx
忘了加jmx 启动参数?attach 进目标进程,在agent 里加上调起jmx 服务的代码即可。
3.2.2 查看内存
agent 里实现动态查看内存的功能,轻一点的方式可以通过重载Class,自带的Javascript引擎,重一点的方式可以内嵌脚本解释器如Groovy Script Engine,OGNL等。
3.3 综合应用:代码动态监控
综合attach目标进程,热更redefine,transform字节码,再加上ASM操作字节码,可以做到对代码的动态监控。
agent attach方式,将一个完整的执行代码监控的程序附加到目标进程,并打开对应的监听操作端口。当我们要查看进程中某个方法的调用入参,出参,执行耗时的时候,通过下列流程实现监听效果:
1.通过打开的端口,发送指令,诸如:monitor com.game.Util random 表示监听random 方法的调用;
2.通过agent回调的Instrument 对象的#retransformClasses() 拿到未修改前的类的字节码;
3.通过ASM,在对应监听方法random 调用前后、方法体内部每一次调用插入监控字节码,并redefine;
4.监听方法被访问后,回调给监听者,然后重新把修改前的字节码redefine,恢复现场。
整个流程下来,可以做到快捷,无侵入方式实现代码的跟踪。
4. 利用Agent已经实现的功能
项目地址:agent