什么是字节码?
Java
程序通过javac
编译之后生成文件.class
就是字节码集合,正是有这样一种中间码(字节码)
,使得scala/groovy/clojure
等函数语言只用实现一个编译器即可运行在JVM
上。
看看一段简单代码。
public long getExclusiveTime() {
long startTime = System.currentTimeMillis();
System.out.printf("exclusive code");
long endTime = System.currentTimeMillis();
return endTime - startTime;
}
public class com.blueware.agent.StartAgent {
编译后通过命令(javap -c com.blueware.agent.StartAgent
)查看,具体含义请参考oracle
public com.blueware.agent.StartAgent();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public long getExclusiveTime();
Code:
0: invokestatic #2 // Method java/lang/System.currentTimeMillis:()J
3: lstore_1
4: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #4 // String exclusive code
9: iconst_0
10: anewarray #5 // class java/lang/Object
13: invokevirtual #6 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
16: pop
17: invokestatic #2 // Method java/lang/System.currentTimeMillis:()J
20: lstore_3
21: lload_3
22: lload_1
23: lsub
24: lreturn
}
为什么要学习字节码?
- 能了解技术背后的原理,更容易写出高质量代码;
- 字节码设计非常优秀,发展十几年只仅仅删除和增加几个指令,学懂之后长期受益高,如果懂字节码再学习
scala/groovy/clojure
会容易很多; - 开发框架、监控系统、中间件、语言字节码技术都是必杀技;
字节码框架(ASM/Javassist
)
操作字节码框架有很多,具体可以参考博文,下面对比ASM/Javassist
|选项 | 优点 |缺点 |
|--------------|----------|-------------|
|ASM
|速度快、代码量小、功能强大|要写字节码、学习曲线高|
|Javassist
|学习简单,不用写字节码|比ASM
慢,功能少|
Java Instrumentation
介绍
指的是可以用独立于应用程序之外的代理(agent
)程序,agent
程序通过增强字节码动态修改或者新增类,利用这样特性可以设计出更通用的监控、框架、中间件程序,在JVM
启动参数加–javaagent:agent_jar_path/agent.jar
即可运行(在JDK5
及其后续版本才可以),更多关于Instrumentation
知识请参考博文
计算方法执行时间方式
- 直接在代码开始和结束出打印当前时间,相减即可得到;
- 实现一个动态代理,或者借助
Spring/AspectJ
等框架; - 上面两种实现方式都需要修改代码或者配置文件,下面我要介绍方式不仅不需要修改代码,而且效率高;
具体实现方式
1.StartAgent
类必须提供premain
方法,代码如下:
public class StartAgent {
//代理程序入口函数
public static void premain(String args, Instrumentation inst) {
System.out.println("agent begin");
//添加字节码转换器
inst.addTransformer(new PrintTimeTransformer());
System.out.println("agent end");
}
}
2.PrintTimeTransformer
实现一个转换器,代码如下:
//字节码转化器类
public class PrintTimeTransformer implements ClassFileTransformer {
//实现字节码转化接口,一个小技巧建议实现接口方法时写@Override,方便重构
//loader:定义要转换的类加载器,如果是引导加载器,则为 null(在这个小demo暂时还用不到)
//className:完全限定类内部形式的类名称和中定义的接口名称,例如"java.lang.instrument.ClassFileTransformer"
//classBeingRedefined:如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
//protectionDomain:要定义或重定义的类的保护域
//classfileBuffer:类文件格式的输入字节缓冲区(不得修改)
//一个格式良好的类文件缓冲区(转换的结果),如果未执行转换,则返回 null。
@Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
//简化测试demo,直接写待修改的类(com/blueware/agent/TestTime)
if (className != null && className.equals("com/blueware/agent/TestTime")) {
//读取类的字节码流
ClassReader reader = new ClassReader(classfileBuffer);
//创建操作字节流值对象,ClassWriter.COMPUTE_MAXS:表示自动计算栈大小
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
//接受一个ClassVisitor子类进行字节码修改
reader.accept(new TimeClassVisitor(writer, className), 8);
//返回修改后的字节码流
return writer.toByteArray();
}
return null;
}
}
3.TimeClassVisitor
类访问器,实现字节码修改,代码如下:
//定义扫描待修改class的visitor,visitor就是访问者模式
public class TimeClassVisitor extends ClassVisitor {
private String className;
public TimeClassVisitor(ClassVisitor cv, String className) {
super(Opcodes.ASM5, cv);
this.className = className;
}
//扫描到每个方法都会进入,参数详情下一篇博文详细分析
@Override public MethodVisitor visitMethod(int access, final String name, final String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
final String key = className + name + desc;
//过来待修改类的构造函数
if (!name.equals("<init>") && mv != null) {
mv = new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
//方法进入时获取开始时间
@Override public void onMethodEnter() {
//相当于com.blueware.agent.TimeUtil.setStartTime("key");
this.visitLdcInsn(key);
this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/blueware/agent/TimeUtil", "setStartTime", "(Ljava/lang/String;)V", false);
}
//方法退出时获取结束时间并计算执行时间
@Override public void onMethodExit(int opcode) {
//相当于com.blueware.agent.TimeUtil.setEndTime("key");
this.visitLdcInsn(key);
this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/blueware/agent/TimeUtil", "setEndTime", "(Ljava/lang/String;)V", false);
//向栈中压入类名称
this.visitLdcInsn(className);
//向栈中压入方法名
this.visitLdcInsn(name);
//向栈中压入方法描述
this.visitLdcInsn(desc);
//相当于com.blueware.agent.TimeUtil.getExclusiveTime("com/blueware/agent/TestTime","testTime");
this.visitMethodInsn(Opcodes.INVOKESTATIC, "com/blueware/agent/TimeUtil", "getExclusiveTime", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)J", false);
}
};
}
return mv;
}
}
4.TimeClassVisitor
记录时间帮助类,代码如下:
public class TimeUtil {
private static Map<String, Long> startTimes = new HashMap<String, Long>();
private static Map<String, Long> endTimes = new HashMap<String, Long>();
private TimeUtil() {
}
public static long getStartTime(String key) {
return startTimes.get(key);
}
public static void setStartTime(String key) {
startTimes.put(key, System.currentTimeMillis());
}
public static long getEndTime(String key) {
return endTimes.get(key);
}
public static void setEndTime(String key) {
endTimes.put(key, System.currentTimeMillis());
}
public static long getExclusiveTime(String className, String methodName, String methodDesc) {
String key = className + methodName + methodDesc;
long exclusive = getEndTime(key) - getStartTime(key);
System.out.println(className.replace("/", ".") + "." + methodName + " exclusive:" + exclusive);
return exclusive;
}
}
题记
- 上面的代码难免有
bug
,如果你发现代码写的有问题,请你帮忙指出,让我们一起进步,让代码变的更漂亮和健壮; - 顺便打点广告,如果看后对字节码技术感兴趣,欢迎加入我们oneapm,一起做点有意思事情,可直接联系我;
- 完整代码请访问github;
下一篇结合
demo
再深入研究ClassVisitor
OneAPM 为您提供端到端的 Java 应用性能解决方案,我们支持所有常见的 Java 框架及应用服务器,助您快速发现系统瓶颈,定位异常根本原因。分钟级部署,即刻体验,Java 监控从来没有如此简单。想阅读更多技术文章,请访问 OneAPM 官方技术博客,还可以扫码关注下方的Java程序性能优化公众号。
染头发
聚一聚……
染头发
教育局……
你好
嗯讲得一般……
星雾
Lambda表达式的条件限制很多,应用面不多,我不知道是否应该要花时间来掌握这个表达式,求解……
修道小仙
感谢分享,来龙去脉,深入浅出,非常清晰……
小布丁
写的棒棒哒……
小布丁
写的真可以……
wuxin
受教了……
爱码物联
博客使用……
yancy_01
很喜欢文字的描述,特别是理论性质的,相比于代码,理论知识更加有意思,谢谢分享……