网站首页 > 技术文章 正文
大家好,我是方木
前言
动态代理分为两种,JDK动态代理和spring里边使用的Cglib动态代理。分别使用的是interface和子类继承的思路来对委托类进行wrap生成代理类。
一直据说由于JDK动态代理使用的是反射的方式对委托类的方法进行调用,性能低,而cglib使用的是字节码修改的方式,性能高。
本篇就尝试搞清楚低为什么低,而高为什么高。
以下分析环境所用的jdk版本:
java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)
java反射:
- 动态化的调用使得JIT编译优化没法做
- newInstance创建Object,getDeclareMethod,Method.invoke()耗时
ASM,Cglib:
可以直接生成class文件或在class load之前修改class文件
修改class文件 -> 生成$Proxy类 -> load到jvm,这样一个过程,所以第一次会慢一些,但一旦载入jvm之后,就跟普通的Java类一样了,对象的方法调用也是可以被JIT优化的了。
避免大量循环使用反射调用,但如果跟JDBC这种SQL调用一起,那么反射的性能损耗基本可以忽略不记了。
比较Java反射与普通对象方法调用的性能
我们用一个例子来比较一下普通对象方法调用、java反射、基于字节码修改的reflectAsm反射,这几种方法调用方式的性能差别。
import com.esotericsoftware.reflectasm.MethodAccess;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* java反射性能测试
* */
@Slf4j
public class ReflectTest {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
long start , end;
int tenMillion = 10000000;
//1、普通new对象,调用方法
DummyObject obj = new DummyObject();
start = System.currentTimeMillis();
for(int i=0; i<tenMillion; i++){
obj.setValue(i);
}
log.info("普通对象方法调用耗时{}ms" , System.currentTimeMillis() - start);
log.info("value = {}", obj.getValue());
//2、使用反射,method.invoke调用方法
Class clazz = Class.forName("com.wangan.springbootone.aop.ReflectTest$DummyObject");
Class[] argsType = new Class[1];
argsType[0] = int.class;
Method method = clazz.getDeclaredMethod("setValue", argsType);
DummyObject dummyObject = (DummyObject) clazz.newInstance();
start = System.currentTimeMillis();
for(int i=0; i<tenMillion; i++){
method.invoke(dummyObject, i);
}
log.info("反射方法invoke调用耗时{}ms" , System.currentTimeMillis() - start);
log.info("value = {}", dummyObject.getValue());
//3、反射调用,getDeclaredMethod + invoke耗时
start = System.currentTimeMillis();
for(int i=0; i<tenMillion; i++){
method = clazz.getDeclaredMethod("setValue", argsType); //比较耗时
method.invoke(dummyObject, i);
}
log.info("反射方法getDeclaredMethod + invoke调用耗时{}ms" , System.currentTimeMillis() - start);
log.info("value = {}", dummyObject.getValue());
//4、使用reflectAsm高性能反射库invoke调用
MethodAccess methodAccess = MethodAccess.get(DummyObject.class);
int index = methodAccess.getIndex("setValue");
start = System.currentTimeMillis();
for(int i=0; i<tenMillion; i++){
methodAccess.invoke(dummyObject, index, i);
}
log.info("使用reflectasm的invoke调用耗时{}ms" , System.currentTimeMillis() - start);
log.info("value = {}", dummyObject.getValue());
}
public static class DummyObject{
private int value;
public void setValue(int v){
value = v;
}
public int getValue(){
return value;
}
}
}
输出:
11:35:58.720 [main] INFO com.wangan.springbootone.aop.ReflectTest - 普通对象方法调用耗时4ms
11:35:58.787 [main] INFO com.wangan.springbootone.aop.ReflectTest - 反射方法invoke调用耗时62ms
11:35:59.913 [main] INFO com.wangan.springbootone.aop.ReflectTest - 反射方法getDeclaredMethod + invoke调用耗时1126ms
11:35:59.991 [main] INFO com.wangan.springbootone.aop.ReflectTest - 使用reflectasm的invoke调用耗时61ms
对一个DummyObject的set方法调用1千万次,普通方法耗时仅4ms,java反射方法只method.invoke的话是62ms,使用reflectasm的invoke耗时接近、61ms, 最慢的是java反射class.getDeclaredMethod + method.invoke、需要1126ms。
我们可以得出几个阶段性结论:
普通对象方法调用最快如果仅测试method.invoke的话,那么java自己的反射方法调用跟reflectasm的invoke性能差不多所谓java反射性能不行,实际上我们看是慢在getDeclaredMethod上,也就是根据Class对象到方法区里边查找类的方法定义的过程,找到方法定义之后真正method.invoke方法调用其实不算很慢。getDeclaredMethod非常慢、差300倍了,method.invoke跟普通的对象方法调用相比也慢了10几倍差1个数量级的样子。
尝试分析一波原因
Class.getDeclaredMethod方法:
@CallerSensitive
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
//接入校验
checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true);
//方法查找
Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes);
if (method == null) {
throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes));
}
return method;
}
checkMemberAccess校验方法是否允许调用,可见性检查。
privateGetDeclaredMethods方法查找先尝试取缓存,没找到就调用getDeclaredMethods0这个native方法,request value from VM 。使用缓存,这个JNI调用是个相对耗时的操作。
Method.invoke方法:
@CallerSensitive
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) { //参数校验
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
MethodAccessor的实现有java版本和native版本
public MethodAccessor newMethodAccessor(Method var1) {
checkInitted();
if (noInflation && !ReflectUtil.isVMAnonymousClass(var1.getDeclaringClass())) {
// 这里返回的是MethodAccessorImpl
return (new MethodAccessorGenerator()).generateMethod(var1.getDeclaringClass(), var1.getName(), var1.getParameterTypes(), var1.getReturnType(), var1.getExceptionTypes(), var1.getModifiers());
} else {
//否则使用NativeMethodAccessorImpl
NativeMethodAccessorImpl var2 = new NativeMethodAccessorImpl(var1);
DelegatingMethodAccessorImpl var3 = new DelegatingMethodAccessorImpl(var2);
var2.setParent(var3);
return var3;
}
}
//反射调用超过这个次数则使用MethodAccessorImpl,否则默认使用NativeMethodAccessorImpl
inflationThreshold = 15;
Java 反射效率低主要原因
- Method#invoke 方法会对参数做封装和解封操作
- 需要检查方法可见性
- 需要校验参数
- 反射方法难以内联
- JIT 无法优化
- 请求jvm去查找其方法区中的方法定义,需要使用jni、开销相对比较大。
所以cglib使用了FastClass机制来索引类的方法调用。也能实现Java反射的"运行时动态方法调用"的功能。
来源:https://www.cnblogs.com/lyhero11/p/15558956.html
关注我的微信公众号:Java架构师进阶编程 获取最新面试题,电子书
专注分享Java技术干货,包括JVM、SpringBoot、SpringCloud、数据库、架构设计、面试题、电子书等,期待你的关注!
猜你喜欢
- 2024-09-21 Java并发编程:LongAdder | LongAccumulator 对比测试
- 2024-09-21 「Java技巧」优雅的统计程序的执行时间,别再用System.cur
- 2024-09-21 Flink SQL 知其所以然(九)| SQL 的时间语义
- 2024-09-21 ArrayList插入1000w条数据之后,我怀疑了jvm...
- 2024-09-21 《Java实战之内存模型》详解篇(java内存模型happens before)
- 2024-09-21 比反射更快!使用ASM获取class信息(ClassReader)
- 2024-09-21 了解Java线程优先级,更要知道对应操作系统的优先级,不然会踩坑
- 2024-09-21 让大学生写的一个计算时间的方法,有人看得出来是在做什么吗?这
- 2024-09-21 Java基础——Java多线程(Lock接口详解)
- 2024-09-21 JVM性能调优监控工具jps、jstack、jmap、jhat、jstat使用详解
- 1514℃桌面软件开发新体验!用 Blazor Hybrid 打造简洁高效的视频处理工具
- 569℃Dify工具使用全场景:dify-sandbox沙盒的原理(源码篇·第2期)
- 510℃MySQL service启动脚本浅析(r12笔记第59天)
- 486℃服务器异常重启,导致mysql启动失败,问题解决过程记录
- 485℃启用MySQL查询缓存(mysql8.0查询缓存)
- 467℃「赵强老师」MySQL的闪回(赵强iso是哪个大学毕业的)
- 446℃mysql服务怎么启动和关闭?(mysql服务怎么启动和关闭)
- 444℃MySQL server PID file could not be found!失败
- 最近发表
- 标签列表
-
- c++中::是什么意思 (83)
- 标签用于 (65)
- 主键只能有一个吗 (66)
- c#console.writeline不显示 (75)
- pythoncase语句 (81)
- es6includes (73)
- windowsscripthost (67)
- apt-getinstall-y (86)
- node_modules怎么生成 (76)
- c++int转char (75)
- static函数和普通函数 (76)
- el-date-picker开始日期早于结束日期 (70)
- js判断是否是json字符串 (67)
- checkout-b (67)
- c语言min函数头文件 (68)
- asynccallback (71)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- & (66)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- eacces (67)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)