Java Agent探针 学习笔记
Posted Shi Peng
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java Agent探针 学习笔记相关的知识,希望对你有一定的参考价值。
一、前言
当我们遇到线上问题,需要加log来辅助定位问题时,通常需要改代码上线发布,才能输出log来,这样比较麻烦。
思考:是否可以在服务器上改代码让它实时生效呢?答案是可以的!
从java5开始,jdk中新增了一个java.lang.instrument.Instrumentation 类,它提供在运行时重新加载某个类的的class文件的api。
下面是它的一些主要api:
public interface Instrumentation {
/**
* 加入一个转换器Transformer,之后的所有的类加载都会被Transformer拦截。
* ClassFileTransformer类是一个接口,使用时需要实现它,该类只有一个方法,该方法传递类的信息,返回值是转换后的类的字节码文件。
*/
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
/**
* 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
* 该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
*/
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
/**
*此方法用于替换类的定义,而不引用现有的类文件字节,就像从源代码重新编译以进行修复和继续调试时所做的那样。
*在要转换现有类文件字节的地方(例如在字节码插装中),应该使用retransformClasses。
*该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
*/
void redefineClasses(ClassDefinition... definitions)throws ClassNotFoundException, UnmodifiableClassException;
/**
* 获取一个对象的大小
*/
long getObjectSize(Object objectToSize);
/**
* 将一个jar加入到bootstrap classloader的 classpath里
*/
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
/**
* 获取当前被JVM加载的所有类对象
*/
Class[] getAllLoadedClasses();
}
通过addTransformer可以加入一个转换器,转换器可以实现对类加载的事件进行拦截并返回转换后新的字节码,通过redefineClasses或retransformClasses都可以触发类的重新加载事件。通过这几个方法的组合,就可以实现文章开头提到的不修改代码使之实时生效的目的了。
二、Java Agent
通过操作Instrumentation的api就可以实现不重启服务对单个类进行简单的修改。Instrumentation是一个interface,它的实现类InstrumentationImpl只有一个private的构造方法。
怎么拿到这个对象呢?有两种方式拿到Instrumentation对象:
- 在jvm启动时指定agent,Instrumentation对象会通过agent的premain方法传递。
- 在jvm启动后通过jvm提供的机制加载agent,Instrumentation对象会通过agent的agentmain方法传递。
三、java启动时加载agent 获取Instrumentation对象 实践
编写agent类并编译成.class文件,之后把它打成jar包,然后在jvm启动参数中指定jar包位置,具体操作步骤:
1、创建一个agent类,并创建premain方法,premain方法的参数是固定的。
public class preMainAgentClz {
private static Instrumentation instrumentation;
public static void premain(String agentArgs, Instrumentation inst) {
instrumentation = inst;
System.err.println("com.agent.demo1.preMainAgentClz 我在main启动之前启动");
}
}
2、指定premain方法的位置(两种指定方式,设置一种就行)
方式1)创建并编辑 resources/META-INF/MANIFEST.MF 文件,当打jar包时将该文件一并打包
Premain-Class: com.hexuan.agent.demo1.preMainAgentClz #premain方法所在类的位置
方式2)如果是maven项目,在pom.xml加入
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>com.hexuan.agent.demo1.preMainAgentClz</Premain-Class>
<Agent-Class>com.hexuan.agent.demo1.agentMainAgentClz</Agent-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
3、如果是在pom中配置的,直接maven package就好了。如果是MANIFEST.MF文件指定的方式,将包含premain的类编译成class文件,并和MANIFEST.MF一起文件打包jar。
4、启动时指定agent位置,在jvm启动参数中加入-javaagent参数并指定jar文件位置。
-javaagent:/Users/IdeaProjects/acfun_WorkSpace/java-agent-demo/target/java-agent-demo-1.0-SNAPSHOT.jar
5、启动java,agent的premain方法会在main方法之前执行。
四、在java启动后以attach的方式加载agent
上文介绍了java进程启动时加载agent的方式和步骤,通过它在启动之前将指定的类进行替换。但如果要实现文章开头提到的调试线上代码,我们需要在修改了class文件后重启jvm并且设置-javaagent参数,显然这种方式不是我们最想要的。上文提到过我们可以在jvm启动后通过jvm提供的机制加载agent,也就是说我们能够在任何时候去加载agent,然后替换类文件。这个机制就是jdk的attach api。
Attach API是Sun公司提供的一套扩展API,用来向目标JVM"附着"(Attach)代理工具程序的。有了它,开发者可以方便的监控一个JVM,运行一个外加的代理程序,Sun JVM Attach API功能上非常简单,仅提供了如下几个功能:
- 列出当前所有的JVM实例描述
- Attach到其中一个JVM上,建立通信管道
- 让目标JVM加载Agent
Attach Api 对应的代码位置在 com.sun.tools.attach 包,包里边有一个类VirtualMachine,它有两个比较重要方法:
/**
*传递一个进程号作为参数,返回目标jvm进程的vm对象。
*该方法其实是JVM进程之间指令传递的桥梁,底层通过socket进行通信。
*JVM A可以发送一些指令给JVM B,B收到指令之后,可以执行对应的逻辑
* 比如在命令行中经常使用的jstack、jcmd、jps等,很多都是基于这种机制实现的
**/
public static VirtualMachine attach(String var0) throws AttachNotSupportedException, IOException
/**
*该方法允许我们将agent对应的jar文件地址作为参数传递目标jvm
*目标jvm收到该命令后会加载这个agent
**/
public void loadAgent(String var1) throws AgentLoadException, AgentInitializationException, IOException
显然,我们可以创建一个java进程,用它attach到对应的jvm,并加载agent,agent加载后我们的类也就被成功替换了。
五、怎么得到新的类文件
Instrumentation操作的是.class文件,对于我们开发人员来讲,我们看不懂.class文件,更无法直接修改它了。还是考虑文章一开始提到的线上改代码调试的场景,我们知道了如何去替换类,但是如何得到新的.class类文件呢?
方式1:线下修改.java文件 -->编译成.class文件 -->上传到线上机器–>instrument
方式2:线上.class旧文件 -->反编译成.java文件 -->修改java文件 -->编译成.class文件 -->instrument
方式3:通过ASM或其他操作字节码的组件直接修改.class文件–>instrument
…
无论哪种方式,流程太复杂容易出错,有成熟的组件吗?有,Arthas和Btrace
六、Arthas&btrace
BTrace 是基于动态字节码修改技术(Instrumentation)来实现运行时 java 程序的跟踪和替换。大体的原理可以用下面的公式描述:Client(Java compile api + attach api) + Agent(脚本解析引擎 + ASM + JDK6 Instumentation) + Socket其实 BTrace 就是使用了 java attach api 附加 agent.jar ,然后使用脚本解析引擎+asm来重写指定类的字节码,再使用 instrument 实现对原有类的替换。
但是BTrace脚本在使用上有一定的学习成本,如果能把一些常用的功能封装起来,对外直接提供简单的命令即可操作的话,那就再好不过了。2018年9月份阿里开源了自己的Java诊断工具Arthas。Arthas功能非常强大,通过简单的命令行操作即可完成对应功能。究其背后的技术原理,和本文中提到的大致无二。
Btrace开源地址:https://github.com/btraceio/btrace
Arthas开源地址:https://github.com/alibaba/arth
七、总结
java instrument在很多应用领域都发挥着重要的作用,比如:
- apm:(Application Performance Management)应用性能管理。pinpoint、cat、skywalking等都基于Instrumentation实现
- idea的HotSwap、Jrebel等热部署工具
- 应用级故障演练
- Java诊断工具Arthas、Btrace等
java agent加载的时序图:
使用Arthas实现加log调试示例:
#下载arthas agent
wget https://alibaba.github.io/arthas/arthas-boot.jar
#启动agent
java -jar arthas-boot.jar --target-ip 0.0.0.0
#sc:search class 查找类文件
sc *SelectionController
#jad 反编译class 并输出到文件
jad --source-only com.acfun.controller.SelectionController > /tmp/SelectionController.java
#修改源代码
vi /tmp/SelectionController.java
#sc查找加载UserController的ClassLoader -d参数可以打印出类加载的具体信息
sc -d *SelectionController |grep classLoaderHash
#编译源代码 使用mc(Memory Compiler)命令来编译,并且通过-c参数指定ClassLoader
mc -c 3787f831 /tmp/SelectionController.java -d /tmp
#使用redefine命令重新加载新编译好的class
redefine /tmp/com/acfun/controller/SelectionController.class
#redefine成功之后,访问controller,观察代码是否生效
以上是关于Java Agent探针 学习笔记的主要内容,如果未能解决你的问题,请参考以下文章
Java探针技术-JVM的动态agent机制:在main函数启动之后运行agent
Java综合专栏「引领序幕」全链路追踪原理之Java Agent探针的技术介绍(上篇)
Java综合专栏「渐入佳境」全链路追踪原理之Java Agent探针的底层运作原理和分析(中篇)
[原创]java WEB学习笔记61:Struts2学习之路--通用标签 property,uri,param,set,push,if-else,itertor,sort,date,a标签等(代码片段