前言介绍
- JavaAgent是在JDK5之后提供的新特性,又叫叫java代理。开发人员可通过这种机制(Instrumentation)在jvm加载class文件之前修改类的字节码,动态更改类方法实现AOP,提供监控服务如:方法调用时长、jvm内存等。
- 修改字节码领域有三个比较常见的框架;ASM、byte-buddy、javassist,其操作方式和控制粒度不同。
- ASM 更偏向于底层,直接面向字节码编程,需要了解 JVM 虚拟机中指定规范以及对局部变量以及操作数栈的知识。虽然在编写起来比较麻烦,但是它也是性能最好功能最强的字节码操作库。 CGLIB 动态代理使用的就是ASM。
- Javassist与byte-buddy提供了强大的 API,操作使用上更加容易控制,可以在不了解Java字节码规范的前提下修改class文件。
案例
使用javassist统计方法执行耗时
- 引入maven依赖
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>
- 将javassist打包到构建的javaAgent包中
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<compilerArgument>-parameters</compilerArgument>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
<!-- 将javassist包打包到Agent中 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<artifactSet>
<includes>
<include>org.javassist:javassist:jar:</include>
</includes>
</artifactSet>
</configuration>
</plugin>
</plugins>
</build>
- 编写耗时统计,在目标方法字节码前后插入方法耗时统计逻辑
public class ClazzTransform implements ClassFileTransformer {
/** 增强类所在包名白名单 */
private final String BASE_PACKAGE;
public ClazzTransform(String basePackage) {
this.BASE_PACKAGE = basePackage;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classFileBuffer) {
className = className.replace("/", ".");
if ( !className.startsWith(BASE_PACKAGE) ){
return null;
}
try {
CtClass ctKlass = ClassPool.getDefault().get(className);
CtBehavior[] behaviors = ctKlass.getDeclaredBehaviors();
//遍历方法进行增强
for (CtBehavior m : behaviors) {
enhanceMethod(m);
}
byte[] bytes = ctKlass.toBytecode();
//输出修改后的class文件内容
Files.write(Paths.get("D:\\A.class"), bytes);
return bytes;
} catch (Exception e) {
e.printStackTrace();
}
return classFileBuffer;
}
/** 方法增强,添加方法耗时统计 */
private void enhanceMethod(CtBehavior method) throws CannotCompileException {
if ( method.isEmpty() ){
return;
}
String methodName = method.getName();
method.addLocalVariable("start", CtClass.longType);
method.insertBefore("start = System.currentTimeMillis();");
method.insertAfter( String.format("System.out.println(\"%s cost: \" + (System.currentTimeMillis() - start) + \"ms\");", methodName) );
}
}
- 编写探针引导类
类似于java主类的main方法,jvm会调用java agent的premain方法
public class AgentPremain {
public static void premain(String ages, Instrumentation instrumentation){
instrumentation.addTransformer( new ClazzTransform("com.lauor.agent") );
}
//如果没有实现上面的方法,JVM将尝试调用该方法
public static void premain(String agentArgs) {
}
}
- 配置jar包探针引导类
Manifest-Version: 1.0
Premain-Class: com.lauor.agent.AgentPremain
Can-Redefine-Classes: true
- 测试
- 基于jdk11 httpclient调用百度首页
public class AgentTest {
public static void main(String[] args) throws Exception {
timeMonitor();
}
private static void timeMonitor() throws Exception {
HttpRequest request = HttpRequest.newBuilder( URI.create("https://www.baidu.com/") ).GET().build();
HttpResponse<String> rs = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(rs);
}
}
- 配置带有javaagent的启动参数
-javaagent:D:\agent\target\agent-1.1.1-SNAPSHOT.jar=agentArgs
- 调用结果
- 查看修改后的class文件
使用bytebuddy统计方法调用耗时
- 介绍
使用bytebuddy比使用javassist更为简单,使用上类似于Java动态代理模式 - 引入maven依赖
<properties>
<bytebuddy.version>1.11.22</bytebuddy.version>
</properties>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>${bytebuddy.version}</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>${bytebuddy.version}</version>
<scope>test</scope>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<compilerArgument>-parameters</compilerArgument>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
<!-- 将bytebuddy包打包到Agent中 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<artifactSet>
<includes>
<include>net.bytebuddy:byte-buddy:jar:</include>
<include>net.bytebuddy:byte-buddy-agent:jar:</include>
</includes>
</artifactSet>
</configuration>
</plugin>
</plugins>
</build>
- 编写耗时统计逻辑
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
public class MethodMonitor {
@RuntimeType
public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Exception {
long sTime = System.currentTimeMillis();
try {
return callable.call();
} finally {
System.out.println( String.format("%s cost %dms", method.getName(), System.currentTimeMillis() - sTime) );
}
}
}
- 编写探针引导类
public static void premain(String ages, Instrumentation instrumentation){
System.out.println( String.format("invoke premain args=%s", ages) );
runMethodCost(instrumentation);
}
private static void runMethodCost(Instrumentation instrumentation) {
AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> builder
//拦截任意方法
.method(ElementMatchers.any())
//委托
.intercept(MethodDelegation.to(MethodMonitor.class));
new AgentBuilder
.Default()
//指定需要拦截的类
.type(ElementMatchers.nameStartsWith("com.lauor.agent"))
.transform(transformer)
.installOn(instrumentation);
}
- 同javassist方式一样配置探针jar包引导类,并配置带有javaagent的启动参数
- 运行javassist的基于jdk11 httpclient调用百度首页的例子
使用javaagent监控jvm
- 编写代码收集jvm gc与内存信息
public class JvmMonitor {
/** 堆内存手机 */
public void gatherHeap(){
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapMemory = memoryMXBean.getHeapMemoryUsage();
//初始堆内存
BigDecimal initSize = toMb( heapMemory.getInit() );
BigDecimal usedSize = toMb( heapMemory.getUsed() );
BigDecimal maxSize = toMb( heapMemory.getMax() );
//os已分配大小
BigDecimal committedSize = toMb( heapMemory.getCommitted() );
String info = String.format("init:%sMB,max:%sMB,committed:%sMB,used:%sMB",
initSize.floatValue(), maxSize.floatValue(), committedSize.floatValue(), usedSize.floatValue());
System.out.println(info);
}
/** 字节转mb */
private BigDecimal toMb(long bytes){
final long mInBytes = 1024 * 1024;
//四舍五入保留一位小数
BigDecimal value = BigDecimal.valueOf(bytes).divide(BigDecimal.valueOf(mInBytes), 1, RoundingMode.HALF_UP);
return value;
}
/** GC信息收集 */
public void gatherGc(){
List<GarbageCollectorMXBean> gcMXBeans = ManagementFactory.getGarbageCollectorMXBeans();
for (GarbageCollectorMXBean gcMXBean : gcMXBeans) {
//gc总计次数
long count = gcMXBean.getCollectionCount();
//gc名
String name = gcMXBean.getName();
//gc大致耗时
long costInMill = gcMXBean.getCollectionTime();
String memNames = Arrays.deepToString( gcMXBean.getMemoryPoolNames() );
String info = String.format("name:%s,count:%s,cost %dms,pool name:%s",
name, count, costInMill, memNames);
System.out.println(info);
}
}
}
- 编写定时任务,定时执行收集jvm gc与内存信息任务
public class AgentPremain {
public static void premain(String ages, Instrumentation instrumentation){
runJvmTask();
}
private static void runJvmTask(){
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1, new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "pool-jvm-" + threadNumber.getAndIncrement());
if (t.isDaemon()){
t.setDaemon(false);
}
if (t.getPriority() != Thread.NORM_PRIORITY){
t.setPriority(Thread.NORM_PRIORITY);
}
return t;
}
});
JvmMonitor jvmMonitor = new JvmMonitor();
executorService.scheduleAtFixedRate(() -> {
jvmMonitor.gatherGc();
jvmMonitor.gatherHeap();
}, 5, 5, TimeUnit.SECONDS);
}
}
- 同javassist方式一样配置探针jar包引导类,并配置带有javaagent的启动参数
- 编写样例不停创建对象,模拟线上后端应用
public class AgentTest {
public static void main(String[] args) throws Exception {
jvmMonitor();
}
private static void jvmMonitor(){
while (true){
List<String> list = new ArrayList<>();
list.add("print jvm heap");
list.add("print jvm gc");
list.clear();
}
}
}
- 运行结果
- 开箱即用的基于javaagent的监控解决方案:prometheus(普罗米修斯)
- 介绍:
prometheus是一个开源的监控系统和报警工具集合。主要有以下特点:
1:由指标名称和和键/值对标签标识的时间序列数据组成的多维数据模型。
2:强大的查询语言 PromQL。
3:不依赖分布式存储;单个服务节点具有自治能力。
4:时间序列数据是服务端通过 HTTP 协议主动拉取获得的,
也可以通过中间网关来推送时间序列数据。
5:可以通过静态配置文件或服务发现来获取监控目标。
6:支持多种类型的图表和仪表盘。 - prometheus结合grafana的效果图