前言
前面我們進行了代理模式、靜態代理、動態代理的學習,而動態代理就是利用Java的反射技術(Java Reflection),在運行時創建一個實作某些給定介面的新類(也稱“動態代理類”)及其實體(物件),所以接下來我們有必要學習一下Java中的反射,
一、基礎知識
1.1 反射是什么?
在講反射之前,先提一個問題:假如現在有一個類User,我想創建一個User物件并且獲取到其name屬性,我該怎么做呢?
User.java
package com.reflect;
/**
* @author: create by lengzefu
* @description: com.reflect
* @date:2020-09-29
*/
public class User {
private String name = "小明";
Integer age = 18;
public User(){
}
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
方式很簡單:
import com.reflect.User;
public class Main {
public static void main(String[] args) {
User user = new User();
System.out.println(user.getName());
}
}
這種方式是我們日常在寫代碼時經常用到的一種方式,這是因為我們在使用某個物件時,總是預先知道自己需要使用到哪個類,因此可以使用直接 new 的方式獲取類的物件,進而呼叫類的方法,
那假設我們預先并不知道自己要使用的類是什么呢?這種場景其實很常見,比如動態代理,我們事先并不知道代理類是什么,代理類是在運行時才生成的,這種情況我們就要用到今天的主角:反射
1.1.1 反射的定義
JAVA反射機制是指在運行狀態中,對于任意一個類,都能夠知道這個類的所有屬性和方法;對于任意一個物件,都能夠呼叫它的任意一個方法和屬性;這種動態獲取的資訊以及動態呼叫物件的方法的功能稱為java語言的反射機制,
注意:這里特別強調的是運行狀態,
1.2 反射能做什么?
定義已經給了我們答案,反射可以使得程式在運行時可以獲取到任意類的任意屬性和方法并呼叫,
1.3 反射為什么能做到?
這里我們需要講一個“類物件”的概念,java中“面向物件”的理念貫徹的比較徹底,它強調“萬事萬物皆物件”,那么“類”是不是也可以認為是一個物件呢?java中有一種特殊的類:Class,它的物件是“類”,比如“String”類,“Thread”類都是它的物件,
java.lang.Class是訪問型別元資料的介面,也是實作反射的關鍵資料,通過Class提供的介面,可以訪問一個型別的方法、欄位等資訊,
以上已經解答了“反射為什么能做到可以使得程式在運行時可以獲取到任意類的任意屬性和方法并呼叫”的問題,它是依賴.class位元組碼檔案做到的,那么首先我們需要解決的問題是如何獲取位元組碼檔案物件(Class物件),
1.3.1 獲取Class物件
對于一個類,如上文的User,我想獲取User的相關資訊(由于Users屬于Class類的物件,所以一般稱該行為為“獲取類物件”),該怎么做呢?
有以下三種方式
import com.reflect.User;
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
// 1.已實體化的物件可呼叫 getClass() 方法來獲取,通常應用在:傳過來一個 Object型別的物件,不知道具體是什么類,用這種方法
User user = new User();
Class clz1 = user.getClass();
// 2.類名.class 的方式得到,該方法最為安全可靠,程式性能更高,這說明任何一個類都有一個隱含的靜態成員變數 class
Class clz2 = User.class;
// 通過類的全路徑名獲取Class物件,用的最多,如果根據類路徑找不到這個類那么就會拋出 ClassNotFoundException例外,
Class clz3 = Class.forName("com.reflect.User");
// 一個類在 JVM 中只會有一個 Class 實體,即我們對上面獲取的 clz1,clz2,clz3進行 equals 比較,發現都是true,
System.out.println(clz1.equals(clz2));
System.out.println(clz2.equals(clz3));
}
}
1.3.2 Class API
獲取公共構造器 getConstructors()
獲取所有構造器 getDeclaredConstructors()
獲取該類物件 newInstance()
獲取類名包含包路徑 getName()
獲取類名不包含包路徑 getSimpleName()
獲取類公共型別的所有屬性 getFields()
獲取類的所有屬性 getDeclaredFields()
獲取類公共型別的指定屬性 getField(String name)
獲取類全部型別的指定屬性 getDeclaredField(String name)
獲取類公共型別的方法 getMethods()
獲取類的所有方法 getDeclaredMethods()
獲得類的特定公共型別方法: getMethod(String name, Class[] parameterTypes)
獲取內部類 getDeclaredClasses()
獲取外部類 getDeclaringClass()
獲取修飾符 getModifiers()
獲取所在包 getPackage()
獲取所實作的介面 getInterfaces()
具體如何使用不再贅述
二、反射原理決議
2.1 反射與類加載的關系
java類的執行需要經歷以下程序,
編譯:java檔案編譯后生成.class位元組碼檔案
加載:類加載器負責根據一個類的全限定名來讀取此類的二進制位元組流到 JVM 內部,并存盤在運行時記憶體區的方法區,然后將其轉換為一個與目標型別對應的 java.lang.Class 物件實體
鏈接:
- 驗證:格式(class檔案規范) 語意(final類是否有子類) 操作
- 準備:靜態變數賦初值和記憶體空間,final修飾的記憶體空間直接賦原值,此處不是用戶指定的初值,
- 決議:符號參考轉化為直接參考,分配地址
初始化:有父類先初始化父類,然后初始化自己;將static修飾代碼執行一遍,如果是靜態變數,則用用戶指定值覆寫原有初值;如果是代碼塊,則執行一遍操作,
Java的反射就是利用上面第二步加載到jvm中的.class檔案來進行操作的,.第二步加載到jvm中的.class檔案來進行操作的,.class檔案中包含java類的所有資訊,當你不知道某個類具體資訊時,可以使用反射獲取class,然后進行各種操作,
首先我們來看看如何使用反射來實作方法的呼叫的:
public class Main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// 1.獲取類物件
Class clz3 = Class.forName("com.reflect.User");
// 2.獲取類的建構式
Constructor constructor = clz3.getConstructor(String.class, Integer.class);
// 3.創建一個物件
User user = (User)constructor.newInstance("璐璐", 17);
// 4.獲取方法getName
Method method = clz3.getMethod("getName");
// 5.呼叫該方法
String name = (String) method.invoke(user);
System.out.println(name);
}
}
接下來主要決議4,5兩個程序:獲取Method物件和Methode.invoke
2.2 獲取 Method 物件
2.2.1 獲取 Method 的API介紹
Class API中關于獲取Method物件的方法有如下幾個:
getMethod/getMethods 和 getDeclaredMethod/getDeclaredMethod
后綴有無“s”的區別
有“s”表示獲取的所有的,無“s”表示獲取的是特定的(由方法引數指定),
getMethod和getDeclaredMethod的區別
Method對應的是Member.PUBLIC,DeclaredMethod對應的是Member.DECLARED
兩者定義如下:
public
interface Member {
/**
* Identifies the set of all public members of a class or interface,
* including inherited members.
* 標識類或介面的所有公共成員的集合,包括父類的公共成員,
*/
public static final int PUBLIC = 0;
/**
* Identifies the set of declared members of a class or interface.
* Inherited members are not included.
* 標識類或介面所有宣告的成員的集合(public、protected,private),但是不包括父類成員
*/
public static final int DECLARED = 1;
}
其實不管是getMethod還是getDeclaredMethod,底層都呼叫了同一個方法:privateGetDeclaredMethods,因此我們只分析其中一個方法即可,
2.2.2 getMethod 方法原始碼分析
seq1
// 4.獲取方法getName
Method method = clz3.getMethod("getName");
客戶端呼叫Class.getMethod()方法,
seq2
// 引數“name”為方法名,引數“parameterTypes”為方法的引數,由于引數可能有多個且型別不同,所以這里使用到了泛型及可變引數的設定
public Method getMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
// 權限安全檢查,無權限則拋出 SecurityException
checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true);
Method method = getMethod0(name, parameterTypes, true);
// 獲取到的method為空,拋出 NoSuchMethodException 例外
if (method == null) {
throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes));
}
return method;
}
該方法的核心方法是 getMethod0,
seq3
private Method getMethod0(String name, Class<?>[] parameterTypes, boolean includeStaticMethods) {
// 保存介面中的方法,最多只有1個,但MethodArray初始化大小至少為2
MethodArray interfaceCandidates = new MethodArray(2);
// 遞回獲取方法,之所以遞回,正是因為getMethod是需要獲取父類中的方法,與前面關于getMethod的介紹對應
Method res = privateGetMethodRecursive(name, parameterTypes, includeStaticMethods, interfaceCandidates);
// 獲取到本類或父類中的方法,直接回傳結果
if (res != null)
return res;
// Not found on class or superclass directly
// 在本類或父類中沒有找到對應的方法,則嘗試去介面中的方法尋找
// removeLessSpecifics:移除更不具體的方法,保留具有更具體實作的方法
interfaceCandidates.removeLessSpecifics();
return interfaceCandidates.getFirst(); // may be null
}
接下來分析privateGetMethodRecursive
seq4
private Method privateGetMethodRecursive(String name,
Class<?>[] parameterTypes,
boolean includeStaticMethods,
MethodArray allInterfaceCandidates) {
// Must _not_ return root methods
Method res;
// Search declared public methods 搜索本來中宣告的公共方法
if ((res = searchMethods(privateGetDeclaredMethods(true),
name,
parameterTypes)) != null) {
if (includeStaticMethods || !Modifier.isStatic(res.getModifiers()))
// res 不為空,回傳
return res;
}
// Search superclass's methods res為空,繼續向父類搜索
if (!isInterface()) { // 介面必然無父類
Class<? super T> c = getSuperclass();
if (c != null) {
// 遞回呼叫getMethod0,獲取父類的方法實作
if ((res = c.getMethod0(name, parameterTypes, true)) != null) {
return res;
}
}
}
// Search superinterfaces' methods res仍然為空,繼續向介面搜索
Class<?>[] interfaces = getInterfaces();
for (Class<?> c : interfaces)
// 遞回呼叫getMethod0,獲取介面的方法實作
if ((res = c.getMethod0(name, parameterTypes, false)) != null)
allInterfaceCandidates.add(res);
// Not found
return null;
}
該方法原英文注釋翻譯之后的大概意思為:
注意:該例程(作用類似于函式,含義比函式更廣)使用的搜索演算法的目的是等效于
privateGetPublicMethods方法的施加順序,它僅獲取每個類的已宣告公共方法,但是,以減少在要查詢的類中宣告了所請求方法,常見情況下必須創建的Method物件的數量, 由于使用默認方法,除非在超類上找到方法,否則需要考慮在任何超級介面中宣告的方法, 收集所有InterfaceCandidates的超級介面中宣告的所有候選物件,如果未在超類上找到匹配項,則選擇最具體的候選者,
原文的英語注釋中各種從句真的很多,自己翻譯完感徑訓是有點問題,簡單來說,我覺得比較重要的一點應該是還是能理解到:
獲取該類已宣告的方法,如果使用的是默認方法,則從父類中尋找該方法,否則去介面中尋找實作最具體的候選方法
接下來分析searchMethods
seq5
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;
}
return (res == null ? res : getReflectionFactory().copyMethod(res));
}
searchMethods的實作邏輯比較簡單,詳細如注釋,這里關鍵是方法引數Method[] methods是怎么得到的,我們回到searchMethods的方法呼叫處:
searchMethods(privateGetDeclaredMethods(true),
name,
parameterTypes)) != null)
methods通過方法privateGetDeclaredMethods(true)得到
seq6
private Method[] privateGetDeclaredMethods(boolean publicOnly) {
checkInitted();
Method[] res;
// 1.ReflectionData 存盤反射資料的快取結構
ReflectionData<T> rd = reflectionData();
if (rd != null) {
// 2.先從快取中獲取methods
res = publicOnly ? rd.declaredPublicMethods : rd.declaredMethods;
if (res != null) return res;
}
// No cached value available; request value from VM
// 3.沒有快取,通過 JVM 獲取
res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly));
if (rd != null) {
if (publicOnly) {
rd.declaredPublicMethods = res;
} else {
rd.declaredMethods = res;
}
}
return res;
}
先看看ReflectionData<T>
// reflection data that might get invalidated when JVM TI RedefineClasses() is called
private static class ReflectionData<T> {
volatile Field[] declaredFields;
volatile Field[] publicFields;
volatile Method[] declaredMethods;
volatile Method[] publicMethods;
volatile Constructor<T>[] declaredConstructors;
volatile Constructor<T>[] publicConstructors;
// Intermediate results for getFields and getMethods
volatile Field[] declaredPublicFields;
volatile Method[] declaredPublicMethods;
volatile Class<?>[] interfaces;
// Value of classRedefinedCount when we created this ReflectionData instance
final int redefinedCount;
ReflectionData(int redefinedCount) {
this.redefinedCount = redefinedCount;
}
}
ReflectionData<T>是類Class的靜態內部類,<T>表示泛型,為具體的類物件,該快取資料結構中存盤了類的所有資訊,redefinedCount是類的重定義次數,可以理解為快取的版本號,
注意最上面的一行注釋:reflection data that might get invalidated when JVM TI RedefineClasses() is called,意思是 當 JVM TI(工具介面)RedefineClasses()被呼叫時,快取資料可能會失效,
通過以上分析,我們知道,每一個類物件理論上都會有(被垃圾回識訓從來沒被加載過就沒沒有)一個ReflectionData<T>的快取,那么如何獲取它呢?
這就要用到 reflectionData
// Lazily create and cache ReflectionData
private ReflectionData<T> reflectionData() {
// 獲取當前 reflectionData 快取
SoftReference<ReflectionData<T>> reflectionData = https://www.cnblogs.com/cleverziv/archive/2020/10/01/this.reflectionData;
// 當前快取版本號
int classRedefinedCount = this.classRedefinedCount;
ReflectionData rd;
// 可以使用快取&&快取不為空&&快取中版本號與類中記錄的版本號一致則直接回傳快取
if (useCaches &&
reflectionData != null &&
(rd = reflectionData.get()) != null &&
rd.redefinedCount == classRedefinedCount) {
return rd;
}
// else no SoftReference or cleared SoftReference or stale ReflectionData
// -> create and replace new instance
// 創建新的快取資料
return newReflectionData(reflectionData, classRedefinedCount);
}
看看newReflectionData的實作
private ReflectionData<T> newReflectionData(SoftReference<ReflectionData<T>> oldReflectionData,
int classRedefinedCount) {
// 不使用快取則直接回傳null
if (!useCaches) return null;
// 使用while+CAS方式更新資料,創建一個新的ReflectionData,如果更新成功直接回傳
while (true) {
ReflectionData<T> rd = new ReflectionData<>(classRedefinedCount);
// try to CAS it...
if (Atomic.casReflectionData(this, oldReflectionData, new SoftReference<>(rd))) {
return rd;
}
// else retry
// 獲取到舊的reflectionData和classRedefinedCount的值,如果舊的值不為null, 并且快取未失效,說明其他執行緒更新成功了,直接回傳
oldReflectionData = https://www.cnblogs.com/cleverziv/archive/2020/10/01/this.reflectionData;
classRedefinedCount = this.classRedefinedCount;
if (oldReflectionData != null &&
(rd = oldReflectionData.get()) != null &&
rd.redefinedCount == classRedefinedCount) {
return rd;
}
}
}
現在我們回到privateGetDeclaredMethods方法的實作,對于第3步:
// 3.沒有快取,通過 JVM 獲取
res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly));
呼叫的是native方法,此處不再贅述,
在獲取到對應方法以后,并不會直接回傳,如下:
return (res == null ? res : getReflectionFactory().copyMethod(res));
seq7
通過單步除錯可發現getReflectionFactory().copyMethod(res)最終呼叫的是Method#copy
Method copy() {
// 1.該物件的root為null時,表明是 基本方法物件
if (this.root != null)
// 2.只能拷貝基本方法物件即root為null的物件
throw new IllegalArgumentException("Can not copy a non-root Method");
// 3.新建一個與基本方法物件具有相同性質的方法物件
Method res = new Method(clazz, name, parameterTypes, returnType,
exceptionTypes, modifiers, slot, signature,
annotations, parameterAnnotations, annotationDefault);
// 4.res作為this的拷貝,其root屬性必須指向this
res.root = this;
// Might as well eagerly propagate this if already present
// 5.所有的Method的拷貝都會使用同一份methodAccessor
res.methodAccessor = methodAccessor;
return res;
}
注意以下幾點:
- root 屬性:可以理解為每一個 java方法都有唯一的一個Method物件,這個物件就是root,它相當于根物件,對用戶不可見,這個root是不會暴露給用戶的,當我們通過反射獲取Method物件時,新創建Method物件把root包裝起來再給用戶,我們代碼中獲得的Method物件都相當于它的副本(或參考),
- methodAccessor:root 物件持有一個 MethodAccessor 物件,所以所有獲取到的 Method物件都共享這一個 MethodAccessor 物件,因此必須保證它在記憶體中的可見性,
res.root = this:res 作為 this 的拷貝,其 root 屬性必須指向 this,
小結
getMethod方法時序圖

2.3 Method.invoke
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
// 1.檢查權限
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
// 2.獲取 MethodAccessor
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
// 2.1為空時創建MethodAccessor
ma = acquireMethodAccessor();
}
// 3.呼叫 MethodAccessor.invoke
return ma.invoke(obj, args);
}
2.3.1 檢查權限
這里對 override 變數進行判斷,如果 override == true,就跳過檢查 我們通常在 Method#invoke 之前,會呼叫 Method#setAccessible(true),就是設定 override 值為 true,
2.3.2 獲取 MethodAccessor
在上面獲取 Method 的時候我們講到過,Method#copy 會給 Method 的 methodAccessor 賦值,所以這里的 methodAccessor 就是拷貝時使用的 MethodAccessor,如果 ma 為空,就去創建 MethodAccessor,
/*
注意這里沒有使用synchronization, 為給定方法生成一個以上的MethodAccessor是正確的(盡管效率不高), 但是,避免同步可能會使實作更具可伸縮性,
*/
private MethodAccessor acquireMethodAccessor() {
// First check to see if one has been created yet, and take it
// if so
MethodAccessor tmp = null;
if (root != null) tmp = root.getMethodAccessor();
if (tmp != null) {
methodAccessor = tmp;
} else {
// Otherwise fabricate one and propagate it up to the root
tmp = reflectionFactory.newMethodAccessor(this);
setMethodAccessor(tmp);
}
return tmp;
}
這里會先查找 root 的 MethodAccessor,這里的 root 在上面 Method#copy 中設定過,如果還是沒有找到,就去創建 MethodAccessor,
class ReflectionFactory {
public MethodAccessor newMethodAccessor(Method method) {
// 其中會對 noInflation 進行賦值
checkInitted();
// ...
if (noInflation && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
// 生成的是 MethodAccessorImpl
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;
}
}
}
這里可以看到,一共有三種 MethodAccessor,MethodAccessorImpl,NativeMethodAccessorImpl,DelegatingMethodAccessorImpl,采用哪種 MethodAccessor 根據 noInflation 進行判斷,noInflation 默認值為 false,只有指定了 sun.reflect.noInflation 屬性為 true,才會 采用 MethodAccessorImpl,所以默認會呼叫 NativeMethodAccessorImpl,
MethodAccessorImpl 是通過動態生成位元組碼來進行方法呼叫的,是 Java 版本的 MethodAccessor,位元組碼生成比較復雜,這里不放代碼了,大家感興趣可以看這里的 generate 方法,
DelegatingMethodAccessorImpl 就是單純的代理,真正的實作還是 NativeMethodAccessorImpl,
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;
}
}
NativeMethodAccessorImpl 是 Native 版本的 MethodAccessor 實作,
class NativeMethodAccessorImpl extends MethodAccessorImpl {
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
// We can't inflate methods belonging to vm-anonymous classes because
// that kind of class can't be referred to by name, hence can't be
// found from the generated bytecode.
if (++numInvocations > ReflectionFactory.inflationThreshold()
&& !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
// Java 版本的 MethodAccessor
MethodAccessorImpl acc = (MethodAccessorImpl)
new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
parent.setDelegate(acc);
}
// Native 版本呼叫
return invoke0(method, obj, args);
}
private static native Object invoke0(Method m, Object obj, Object[] args);
}
在 NativeMethodAccessorImpl 的實作中,我們可以看到,有一個 numInvocations 閥值控制,numInvocations 表示呼叫次數,如果 numInvocations 大于 15(默認閥值是 15),那么就使用 Java 版本的 MethodAccessorImpl,
為什么采用這個策略呢,可以 JDK 中的注釋:
// "Inflation" mechanism. Loading bytecodes to implement
// Method.invoke() and Constructor.newInstance() currently costs
// 3-4x more than an invocation via native code for the first
// invocation (though subsequent invocations have been benchmarked
// to be over 20x faster). Unfortunately this cost increases
// startup time for certain applications that use reflection
// intensively (but only once per class) to bootstrap themselves.
// To avoid this penalty we reuse the existing JVM entry points
// for the first few invocations of Methods and Constructors and
// then switch to the bytecode-based implementations.
//
// Package-private to be accessible to NativeMethodAccessorImpl
// and NativeConstructorAccessorImpl
private static boolean noInflation = false;
Java 版本的 MethodAccessorImpl 呼叫效率比 Native 版本要快 20 倍以上,但是 Java 版本加載時要比 Native 多消耗 3-4 倍資源,所以默認會呼叫 Native 版本,如果呼叫次數超過 15 次以后,就會選擇運行效率更高的 Java 版本,那為什么 Native 版本運行效率會沒有 Java 版本高呢?從 R 大博客來看,是因為 這是HotSpot的優化方式帶來的性能特性,同時也是許多虛擬機的共同點:跨越native邊界會對優化有阻礙作用,它就像個黑箱一樣讓虛擬機難以分析也將其行內,于是運行時間長了之后反而是托管版本的代碼更快些,
2.3.3 呼叫 MethodAccessor#invoke 實作方法的呼叫
在生成 MethodAccessor 以后,就呼叫其 invoke 方法進行最終的反射呼叫,這里我們對 Java 版本的 MethodAccessorImpl 做個簡單的分析,Native 版本暫時不做分析,在前面我們提到過 MethodAccessorImpl 是通過 MethodAccessorGenerator#generate 生成動態位元組碼然后動態加載到 JVM 中的,
到此,基本上 Java 方法反射的原理就介紹完了,
三、反射為什么慢?
3.1 為什么慢?
Java實作的版本在初始化時需要較多時間,但長久來說性能較好;native版本正好相反,啟動時相對較快,但運行時間長了之后速度就比不過Java版了,這是HotSpot的優化方式帶來的性能特性,同時也是許多虛擬機的共同點:跨越native邊界會對優化有阻礙作用,它就像個黑箱一樣讓虛擬機難以分析也將其行內,于是運行時間長了之后反而是托管版本的代碼更快些, 為了權衡兩個版本的性能,Sun的JDK使用了“inflation”的技巧:讓Java方法在被反射呼叫時,開頭若干次使用native版,等反射呼叫次數超過閾值時則生成一個專用的MethodAccessor實作類,生成其中的invoke()方法的位元組碼,以后對該Java方法的反射呼叫就會使用Java版, 當該反射呼叫成為熱點時,它甚至可以被行內到靠近Method.invoke()的一側,大大降低了反射呼叫的開銷,而native版的反射呼叫則無法被有效行內,因而呼叫開銷無法隨程式的運行而降低,
總結來說,原因如下:
- jit 無法優化反射 , JIT編譯器無法對反射有效做優化,參考一段java doc中的解釋:
由于反射涉及動態決議的型別,導致無法執行某些Java虛擬機優化,所以,反射操作的性能比非反射操作慢,因此應避免在對性能敏感的應用程式中使用反射
- 引數的封裝/解封和方法的校驗,invoke方法是傳Object型別的,如果是簡單型別如long,在介面處必須封裝成object,從而生成大量的Long的Object,導致了額外的不必要的記憶體浪費,甚至有可能導致GC;需要進行引數校驗和方法的可見性校驗,
- 難以行內
3.2 反射慢為什么還要用它?
反射這種技術被廣泛應用于框架的設計中,但反射的確帶來了一定的性能損耗,既然如此為什么還要用反射呢?
- 絕大部分系統的性能瓶頸還遠遠沒有到需要考慮反射這里,邏輯層和資料層上的優化對性能的提升比優化反射高n個數量級,
- 框架的設計是性能、標準和開發效率等多個方面的權衡,
- 反射多少會有性能損耗,但一般可以忽略,而java對javabean方面的反射支持,java底層都有PropertyDescriptor和MethodDescriptor支持,可以一定程度的減少反射消耗, AOP方面,cglib是通過類的位元組碼生成其子類去操作的,一旦子類生成就是純粹的反射呼叫,不再操作位元組碼了,而一般AOP呼叫是在單例上,不會頻繁的去用cglib生成子類,
關于反射性能的具體測驗資料,可參考:https://www.jianshu.com/p/4e2b49fa8ba1
其實通過以上資料可以看出,當量非常大的時候,反射確實是會影響性能,但一般的應用,即使不借助高性能工具包也不會是程式掣肘,當然,這也不是意味著可以隨意使用,還是要結合實際的應用來,
四、反射優缺點
4.1 反射的優點
- 賦予程式在運行時可以操作物件的能力,
- 解耦,提高程式的可擴展性,
4.2 反射的缺點
- **性能開銷
反射涉及型別動態決議,所以JVM無法對這些代碼進行優化,因此,反射操作的效率要比那些非反射操作低得多,我們應該避免在經常被執行的代碼或對性能要求很高的程式中使用反射, - 安全限制
使用反射技術要求程式必須在一個沒有安全限制的環境中運行,如果一個程式必須在有安全限制的環境中運行,如Applet,那么這就是個問題了, - 內部曝光
由于反射允許代碼執行一些在正常情況下不被允許的操作(比如訪問私有的屬性和方法),所以使用反射可能會導致意料之外的副作用--代碼有功能上的錯誤,降低可移植性,反射代碼破壞了抽象性,因此當平臺發生改變的時候,代碼的行為就有可能也隨著變化,
4.3 原則
如果使用常規方法能夠實作,那么就不要用反射,
五、參考文獻
https://www.jianshu.com/p/607ff4e79a13
https://www.cnblogs.com/chanshuyi/p/head_first_of_reflection.html
https://blog.csdn.net/zhenghongcs/article/details/103143144
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/144128.html
標籤:其他
