JAVA基礎:反射
什麼是反射?
反射可以在執行時動態載入類,建立物件。這種類和物件在編譯期間都是不確定的,只有在執行到對應的反射程式碼才能確定下來。反射還可以獲取物件自身的資訊,包括屬性、方法等,並且可以打破訪問修飾符的限制,設定並訪問私有的屬性,獲取並呼叫私有的方法。
通過反射載入類
反射,最剛開始接觸其實是在jdbc的學習中。
看下面獲取mysql連線的程式碼
//反射載入mysql驅動 Class.forName(“com.mysql.jdbc.Driver”); String url = “jdbc:mysql://localhost:3306/test?user=root&password=123456″; Connection con = DriverManager.getConnection(url);
Class.forName用來載入com.mysql.jdbc.Driver這個驅動類,並且返回Class物件,但是這裡並沒有去接收它的返回值,那麼DriverMannger是如何得到這個驅動類的呢?
看下com.mysql.jdbc.Driver的實現就知道了
static { try { DriverManager.registerDriver(new Driver()); } catch (SQLException var1) { throw new RuntimeException("Can't register driver!"); } }
可以看到mysql驅動類中有這一段靜態塊,在載入驅動類時,初始化的階段會執行靜態塊裡的程式碼,載入驅動類到DriverManager.
此外對於Class.forName需要了解一點就是,通過呼叫方的類載入器反射載入該類。
通過反射建立物件
Class clazz = Class.forName("com.domain.Person"); //建立物件,注意必須要有空構造器 Object obj = clazz.newInstance(); //兩種方法判斷某個物件時某一個類的物件 System.out.println(obj instanceof Person); System.out.println(clazz.isInstance(obj));
通過反射獲取物件的域
Class clazz = Class.forName("com.domain.Person");
Object obj = clazz.newInstance();
//可獲取到私有域
//getField只可獲得公開域
Field field = clazz.getDeclaredField("name");
//私有域 可以設定為可以訪問,打破訪問修飾符的訪問控制權限
field.setAccessible(true);
//為obj的name屬性設定值 yang
field.set(obj,"yang");
//獲取輸出
System.out.println(field.get(obj));
//靜態域
Field field1 = clazz.getField("type");
field1.set(clazz,"人");
System.out.println(field1.get(clazz));
通過反射呼叫物件的方法
//獲取方法,可以獲取到私有方法
Method method = clazz.getDeclaredMethod("show",int.class);
// 私有方法設定可訪問,打破訪問修飾符的訪問許可權控制
// method.setAccessible(true);
//執行方法
method.invoke(obj,1);
//靜態方法
Method method1 = clazz.getDeclaredMethod("say",String.class);
method1.invoke(clazz,"hello");
method深入
1、method的get方法
//這個方法讀取快取找那個的 method物件,通過public only來過濾私有和公開方法
private Method[] privateGetDeclaredMethods(boolean publicOnly) {
checkInitted();
Method[] res;
ReflectionData<T> rd = reflectionData();
if (rd != null) {
res = publicOnly ? rd.declaredPublicMethods : rd.declaredMethods;
if (res != null) return res;
}
// No cached value available; request value from VM
res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly));
if (rd != null) {
if (publicOnly) {
rd.declaredPublicMethods = res;
} else {
rd.declaredMethods = res;
}
}
return res;
}
SoftReference<ReflectionData<T>> reflectionData = this.reflectionData;
reflectionData是軟引用物件,在記憶體不足時會被回收,如果被回收了在需要用到他,又需要從jvm中取一把。
把從快取中取出並過濾好的方法陣列拿出來,按要求的方法簽名去找對應方法
private static Method searchMethods(Method[] methods,
String name,
Class<?>[] parameterTypes)
{
Method res = null;
String internedName = name.intern();
for (int i = 0; i < methods.length; i++) {
Method m = methods[i];
if (m.getName() == internedName
&& arrayContentsEq(parameterTypes, m.getParameterTypes())
&& (res == null
|| res.getReturnType().isAssignableFrom(m.getReturnType())))
res = m;
}
//注意下面這個判斷,不要看反了,
//這裡是說,如果沒有找到這個方法,直接返回出去null。否則就copy這個方法返回出去,
//所以由此可知,每次取method,得到的都不是同一個物件,所以能複用method就儘量複用
return (res == null ? res : getReflectionFactory().copyMethod(res));
}
ok,看到這裡,我試了一下,取兩次method,然後輸出hashCode一看,,居然一樣,不信邪,又來幾次,還是一樣,一度懷疑人生。
後來仔細一看才發現method重寫了hashCode,它的hashCode跟他的簽名有關,跟地址無關。
所以,判斷兩個物件是否是同一個物件,堅決不能用hashCode。
要用==,
沒錯用==就明顯對了。
2、method的invoke方法
invoke最終呼叫到MethodAccessor 的invoke
public interface MethodAccessor {
Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException;
}
method裡有個root物件,也就是快取中的method,也就是這個method是通過root複製出來的,如果root裡有MethodAccessor ,就直接使用,否則就需要建立。
MethodAccessor 有三個實現類:
DelegatingMethodAccessorImpl
NativeMethodAccessorImpl
GeneratedMethodAccessorXXX
其中DelegatingMethodAccessorImpl是最終注入到method的MethodAccessor中的,也就是最終要呼叫到DelegatingMethodAccessorImpl的invoke,DelegatingMethodAccessorImpl作為一個代理類存在,委託執行NativeMethodAccessorImpl 或者 GeneratedMethodAccessorXXX【反射生成】的invoke
class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
private MethodAccessorImpl delegate;
DelegatingMethodAccessorImpl(MethodAccessorImpl var1) {
this.setDelegate(var1);
}
public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
return this.delegate.invoke(var1, var2);
}
void setDelegate(MethodAccessorImpl var1) {
this.delegate = var1;
}
}
ok,現在的核心在NativeMethodAccessorImpl
class NativeMethodAccessorImpl extends MethodAccessorImpl {
private final Method method;
private DelegatingMethodAccessorImpl parent;
private int numInvocations;
NativeMethodAccessorImpl(Method var1) {
this.method = var1;
}
public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
//在15次之後,設定對應的代理物件為GeneratedMethodAccessorXXX,
//這個反射得到的類中invoke,其實就是普通的方法呼叫
if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
this.parent.setDelegate(var3);
}
//在方法執行的前15次預設都是呼叫原生的native方法
return invoke0(this.method, var1, var2);
}
void setParent(DelegatingMethodAccessorImpl var1) {
this.parent = var1;
}
private static native Object invoke0(Method var0, Object var1, Object[] var2);
}
其中我上面說的是15次就是ReflectionFactory.inflationThreshold()這個方法返回的,這個15當然也不是一塵不變的,我們可以通過-Dsun.reflect.inflationThreshold=xxx來指定,我們還可以通過-Dsun.reflect.noInflation=true來直接繞過上面的15次NativeMethodAccessorImpl呼叫,和-Dsun.reflect.inflationThreshold=0的效果一樣的。
為什麼要有兩種方式去執行方法的呼叫?
1、原生的在啟動時效能較好,時間長了,效能就會變差
2、反射生成類的在啟動時效能不好,因為需要去反射生成類,啟動一段時間後,效能就很好了。
所以兩者進行了結合,先使用原生,一段時間後,使用反射生成類。
併發呼叫method產生執行緒安全問題?
在呼叫15次後,生成反射類來實現呼叫的方法並沒有加鎖,也就是並不是執行緒安全的。假設在外部method的呼叫層也沒有進行鎖的控制,那麼就會出現一些問題。
假設1000個執行緒同時進來,反射類,那麼就會生成1000個類,雖然最後留下來的就一個類,而且程式也都能正常秩序,但是會有999個類在未被解除安裝之前會佔用記憶體,在極端情況下就會OOM。
GeneratedMethodAccessorXXX如何解除安裝?
首先明確兩點:
1、GeneratedMethodAccessorXXX 需要被解除安裝,為什麼?
假設GeneratedMethodAccessorXXX 都不被解除安裝,當程式中大量使用反射呼叫物件時,很可能因為GeneratedMethodAccessorXXX 的大量生產而導致OOM
2、JVM虛擬機器自帶的類載入器能不能解除安裝類?
在JVM執行期間,JVM會持有預設的類載入器,預設的類載入器又會持有他們載入的Class物件,所以這些Class物件都是可以觸及的,所以在執行期間不可能解除安裝類。
那麼如何實現GeneratedMethodAccessorXXX 的解除安裝,最簡單的方式就是自定義類載入器去實現GeneratedMethodAccessorXXX 的載入,因為使用者自定義的類載入器所載入的類可以被解除安裝。
比如JSP類載入器,每個JSP對應一個類載入器,當發現有JSP修改,就會解除安裝原來的類載入器和類,重新建立類載入器,載入修改後的jsp所編譯的位元組碼檔案。
static Class<?> defineClass(String var0, byte[] var1, int var2, int var3, final ClassLoader var4) {
ClassLoader var5 = (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
public ClassLoader run() {
//可以看到GeneratedMethodAccessorXXX 確實使用自定義的類載入器 DelegatingClassLoader
return new DelegatingClassLoader(var4);
}
});
return unsafe.defineClass(var0, var1, var2, var3, var5, (ProtectionDomain)null);
}