如何執(zhí)行反射調(diào)用
Java的反射調(diào)用是通過java.lang.reflect.Method的invoke調(diào)用執(zhí)行樊销,Method實(shí)例通過反射執(zhí)行的方法的類的Class實(shí)例提供的方法獲取,如下:
package com.test;
import java.lang.reflect.Method;
public class ReflectTest {
public void method1(int arg) {
new RuntimeException("xxxxxxxx").printStackTrace();;
}
public static void main(String[] args) throws Exception {
//獲取需要反射執(zhí)行的方法的Method的實(shí)例
Method method = ReflectTest.class.getMethod("method1", int.class);
//執(zhí)行method的invoke方法進(jìn)行反射調(diào)用,需要傳入反射方法的定義的實(shí)例媳危,以及方法的全部入?yún)ⅲò凑諈?shù)定義的順序)
method.invoke(new ReflectTest(), 128);
}
}
輸出:
java.lang.RuntimeException: xxxxxxxx
at com.test.ReflectTest.method1(ReflectTest.java:8)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.test.ReflectTest.main(ReflectTest.java:13)
從反射調(diào)用的異常調(diào)用棧可以得知呐舔,Method實(shí)例的反射執(zhí)行經(jīng)過了sun.reflect.NativeMethodAccessorImpl的調(diào)用,但這只是通用的情況烹棉,下面會具體分析基于Method反射執(zhí)行的實(shí)現(xiàn)原理。
Method反射調(diào)用的原理
查閱 Method.invoke(jdk1.8.0) 的源代碼怯疤,
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);
}
Method.invoke實(shí)際上委派給 MethodAccessor來處理浆洗。MethodAccessor 是一個(gè)接口,它有兩個(gè)已有的具體實(shí)現(xiàn):一個(gè)通過native code來實(shí)現(xiàn)反射調(diào)用旅薄,另一個(gè)則使用了java本身辅髓。在第一次調(diào)用一個(gè)實(shí)際Java方法對應(yīng)得Method對象的invoke()方法之前,實(shí)現(xiàn)調(diào)用邏輯的MethodAccessor對象還沒創(chuàng)建少梁;等第一次調(diào)用時(shí)才新創(chuàng)建MethodAccessor并更新給root洛口,然后調(diào)用MethodAccessor.invoke()真正完成反射調(diào)用。
委派的MethodAccessor類創(chuàng)建過程關(guān)鍵代碼如下:
if (noInflation) {
return new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
} else {
NativeMethodAccessorImpl acc =
new NativeMethodAccessorImpl(method);
DelegatingMethodAccessorImpl res =
new DelegatingMethodAccessorImpl(acc);
acc.setParent(res);
return res;
}
如果noInflation標(biāo)志位為false凯沪,將會創(chuàng)建DelegatingMethodAccessorImpl實(shí)現(xiàn)類第焰,DelegatingMethodAccessorImpl又是代理了NativeMethodAccessorImpl(native code實(shí)現(xiàn)的MethodAccessor)。DelegatingMethodAccessorImpl:
class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
private MethodAccessorImpl delegate;
DelegatingMethodAccessorImpl(MethodAccessorImpl delegate) {
setDelegate(delegate);
}
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
return delegate.invoke(obj, args);
}
void setDelegate(MethodAccessorImpl delegate) {
this.delegate = delegate;
}
}
為了權(quán)衡兩個(gè)版本的性能妨马,Sun的JDK使用了“inflation”的技巧:讓Java方法在被反射調(diào)用時(shí)挺举,開頭若干次使用native版,等反射調(diào)用次數(shù)超過閾值時(shí)則生成一個(gè)專用的MethodAccessor實(shí)現(xiàn)類烘跺,生成其中的invoke()方法的字節(jié)碼湘纵,以后對該Java方法的反射調(diào)用就會使用Java版。
設(shè)置切換MethodAccessor的實(shí)現(xiàn)類的閥值參數(shù)為:-Dsun.reflect.inflationThreshold 滤淳。反射調(diào)用的 Inflation 機(jī)制是可以通過參數(shù)(-Dsun.reflect.noInflation=true)來關(guān)閉的梧喷。這樣一來,在反射調(diào)用一開始便會直接生成Java實(shí)現(xiàn)的MethodAccessor脖咐。
Java版本的MethodAccessor實(shí)現(xiàn)大致如下(基于開頭的com.test.ReflectTest類版本):
package sun.reflect;
public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
public GeneratedMethodAccessor1() {
super();
}
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException {
// prepare the target and parameters
if (obj == null) throw new NullPointerException();
try {
com.test.ReflectTest target = (com.test.ReflectTest) obj;
if (args.length != 1) throw new IllegalArgumentException();
int arg0 = (int) args[0];
} catch (ClassCastException e) {
throw new IllegalArgumentException(e.toString());
} catch (NullPointerException e) {
throw new IllegalArgumentException(e.toString());
}
// make the invocation
try {
target.method1(arg0);
} catch (Throwable t) {
throw new InvocationTargetException(t);
}
}
}
可見Java版本的MethodAccessor實(shí)現(xiàn)铺敌,其反射調(diào)用可以認(rèn)為是對目標(biāo)方法的invokevirtual調(diào)用。當(dāng)該反射調(diào)用成為熱點(diǎn)時(shí)屁擅,它甚至可以被內(nèi)聯(lián)到靠近Method.invoke()的一側(cè)偿凭,大大降低了反射調(diào)用的開銷。而native版的反射調(diào)用則無法被有效內(nèi)聯(lián)派歌,因而調(diào)用開銷無法隨程序的運(yùn)行而降低弯囊。
兩種實(shí)現(xiàn)的性能比較
- Java實(shí)現(xiàn)的版本在初始化時(shí)需要較多時(shí)間,但是執(zhí)行性能較好
- native版本正好相反硝皂,啟動(dòng)時(shí)相對較快
- native版本將會阻礙JVM的優(yōu)化(跨越native邊界會對優(yōu)化有阻礙作用常挚,它就像個(gè)黑箱一樣讓虛擬機(jī)難以分析也將其內(nèi)聯(lián))
性能優(yōu)化
Java版本的MethodAccessor實(shí)現(xiàn)的反射調(diào)用的性能之所以得到優(yōu)化,主要是因?yàn)榧磿r(shí)編譯器中的方法內(nèi)聯(lián)稽物。在關(guān)閉了 Inflation 的情況下奄毡,內(nèi)聯(lián)的瓶頸在于 Method.invoke 方法中對 MethodAccessor.invoke 方法的調(diào)用。由于 Java 虛擬機(jī)的關(guān)于上述調(diào)用點(diǎn)的類型 profile(注:對于 invokevirtual 或者 invokeinterface贝或,Java 虛擬機(jī)會記錄下調(diào)用者的具體類型吼过,我們稱之為類型 profile)無法同時(shí)記錄這么多個(gè)類锐秦,因此可能造成所測試的反射調(diào)用沒有被內(nèi)聯(lián)的情況。
下面比較因?yàn)檎{(diào)用點(diǎn)的類型profile超出閥值導(dǎo)致JVM無法內(nèi)聯(lián)優(yōu)化熱點(diǎn)反射調(diào)用的性能損耗盗忱,先是看看能夠內(nèi)聯(lián)優(yōu)化的情況:
public static void main(String[] args) throws Exception {
Method method = ReflectTest.class.getMethod("method1", int.class);
// polluteProfile();
ReflectTest obj = new ReflectTest();
long cur = System.currentTimeMillis();
for(int i=0;i<2000000000;i++) {
if(i % 100000000 == 0) {
long tmp = System.currentTimeMillis();
System.out.println("cost=" + (tmp - cur));
cur = tmp;
}
method.invoke(obj, 128);
}
最后五次耗時(shí)統(tǒng)計(jì)輸出:
cost=980
cost=1150
cost=1132
cost=1341
cost=1417
以下是通過創(chuàng)建被反射的類多個(gè)方法的Method對象并存的場景酱床,擾亂JVM的熱點(diǎn)內(nèi)聯(lián)優(yōu)化:
//擾亂內(nèi)聯(lián)優(yōu)化的方法
private static void polluteProfile() throws Exception {
Method method1 = ReflectTest.class.getMethod("method2", int.class);
Method method2 = ReflectTest.class.getMethod("method3", int.class);
ReflectTest obj = new ReflectTest();
for(int i=0;i<2000;i++) {
method1.invoke(obj, 1);
method1.invoke(obj, 2);
}
}
public static void main(String[] args) throws Exception {
Method method = ReflectTest.class.getMethod("method1", int.class);
polluteProfile();
ReflectTest obj = new ReflectTest();
long cur = System.currentTimeMillis();
for(int i=0;i<2000000000;i++) {
if(i % 100000000 == 0) {
long tmp = System.currentTimeMillis();
System.out.println("cost=" + (tmp - cur));
cur = tmp;
}
method.invoke(obj, 128);
}
}
最后五次耗時(shí)統(tǒng)計(jì)輸出:
cost=5545
cost=5521
cost=5488
cost=5536
cost=5488
可以提高 Java 虛擬機(jī)關(guān)于每個(gè)調(diào)用能夠記錄的類型數(shù)目(對應(yīng)虛擬機(jī)參數(shù) -XX:TypeProfileWidth,默認(rèn)值為 2趟佃,實(shí)測-XX:TypeProfileWidth=3作為JVM參數(shù)在本地筆記本無效)扇谣。