前言
動態代理分為兩種,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