java反射真的慢麼?動態代理會創建很多臨時class?

問題

  • 1.反射真的慢麼?
  • 2.動態代理會創建很多臨時class?
  • 3.屬性通過反射讀取怎麼實現的?

當我們在IDE中編寫代碼的時候,打一個點號,IDE會自動彈出對應的屬性和方法名,當我們在debug的時候,IDE會將方法運行時方法內局部變量和外部實例上屬性的值都展示出來,spring中的IOC和AOP,以及一個RPC框架中,我們反序列化,consumer的代理,以及provider的調用都會用到java的反射功能,有人說使用反射會慢,那麼到底慢在哪裡呢?

反射

反射使JAVA語言有了動態編譯的功能,也就是在我們編碼的時候不需要知道對象的具體類型,但是在運行期可以通過Class.forName()獲取一個類的class對象,在通過newInstance獲取實例。

先看下java.lang.reflect包下的幾個主要類的關係圖,當然動態代理的工具類也在該包下。

java反射真的慢麼?動態代理會創建很多臨時class?

AnnotatedElement

作為頂級接口,這個接口提供了獲取註解相關的功能,我們在方法,類,屬性,構造方法上都可以加註解,所以下面的Field,Method,Constructor都有實現這個接口,以下是我們經常用的兩個方法,jdk8以後,接口裡面可以通過default修飾方法實現了

java反射真的慢麼?動態代理會創建很多臨時class?

  • GenericDeclaration

提供了獲取泛型相關的功能,只有方法和構造方法上支持泛型,所以只有Method,Constructor實現了該接口

  • Member

作為一個對象內部方法和屬性的聲明的抽象,包含了名稱,修飾符,所在的類,其中修飾符包含了 static final public private volatile 等,通過一個整數表示,每一個類型在二進制中佔一個位.

java反射真的慢麼?動態代理會創建很多臨時class?

AccessibleObject

這是一個類,提供了權限管理的功能,例如是否允許在反射中在外部調用一個private方法,獲取一個private屬性的值,所以method,constructor,field都繼承該類,下面這段代碼展示瞭如何在反射中訪問一個私有的成員變量,class對象的構造方法不允許對外.

java反射真的慢麼?動態代理會創建很多臨時class?

以下為 Field裡面通過field.get(原始對象)獲取屬性值得實現,先通過override做校驗,如果沒有重載該權限,則需要校驗訪問權限

java反射真的慢麼?動態代理會創建很多臨時class?

下面我們看看如何通過反射修改Field裡面屬性的值

通過上面的代碼,我們可以看出jdk將Field屬性的讀取和寫入委託給FieldAccessor,那麼如何獲取FieldAccessor呢

java反射真的慢麼?動態代理會創建很多臨時class?

java反射真的慢麼?動態代理會創建很多臨時class?

以上代碼可以發現,通過工廠模式根據field屬性類型以及是否靜態來獲取,為什麼會有這樣的劃分呢

首先,jdk是通過UNSAFE類對堆內存中對象的屬性進行直接的讀取和寫入,要讀取和寫入首先需要確定屬性所在的位置,也就是相對對象起始位置的偏移量,而靜態屬性是針對類的不是每個對象實例一份,所以靜態屬性的偏移量需要單獨獲取

其實通過該偏移量我們可以大致推斷出一個實例內每個屬性在堆內存的相對位置,以及分別佔用多大的空間,有了位置信息,我們還需要這個字段的類型,以方便執行器知道讀幾個字節的數據,並且如何進行解析,目前提供了8大基礎類型(char vs Charector)和數組和普通引用類型。 java虛擬機為了保證每個對象所佔的空間都是8個字節倍數,有時候為了避免兩個volatile字段存放在同一個緩存行,所以有時候會再某些字段上做空位填充

以下為UnSafe類的部分代碼

java反射真的慢麼?動態代理會創建很多臨時class?

}

然後我們在來看看通過反射來調用方法

同樣,jdk通過MethodAccessor來進行method的調用,java虛擬機提供了兩種模式來支持method的調用 一個是NativeMethodAccessorImpl 一個是通過ASM字節碼直接動態生成一個類在invoke方法內部調用目標方法,由於是動態生成所以jdk中沒有其源碼,但jdk提供了DelegatingMethodAccessorImpl委派模式以方便在運行過程中可以動態切換字節碼模式和native模式,我們可以看下生成MethodAccessor的代碼

java反射真的慢麼?動態代理會創建很多臨時class?

可以看到JDK內部通過numInvocations判斷如果該反射調用次數超過ReflectionFactory.inflationThreshold()則用字節碼實現,如果小於該值則採用native實現,native的調用比字節碼方式慢很多, 動態實現和本地實現相比執行效率要快20倍,因為動態實現無需經過JAVA,C++再到JAVA的轉換,之前在jdk6以前有個工具ReflectAsm就是採用這種方式提升執行效率,不過在jdk8以後,也提供了字節碼方式,由於許多反射只需要執行一次,然而動態方式生成字節碼十分耗時,所以jdk提供了一個閾值默認15,當某個反射的調用次數小於15的話就走本地實現,大於15則走動態模式,而這個閾值可以在jdk啟動參數裡面做配置

反射為什麼慢

經過以上優化,其實反射的效率並不慢,在某些情況下可能達到和直接調用基本相同的效率,但是在首次執行或者沒有緩存的情況下還是會有性能上的開銷,主要在以下方面

  1. Class.forName();會調用本地方法,我們用到的method和field都會在此時加載進來,雖然會進行緩存,但是本地方法免不了有JAVA到C+=在到JAVA得轉換開銷
  2. class.getMethod(),會遍歷該class所有的公用方法,如果沒匹配到還會遍歷父類的所有方法,並且getMethods()方法會返回結果的一份拷貝,所以該操作不僅消耗CPU還消耗堆內存,在熱點代碼中應該儘量避免,或者進行緩存
  3. invoke參數是一個object數組,而object數組不支持java基礎類型,而自動裝箱也是很耗時的

反射的運用

  • spring ioc

spring加載bean的流程基本都用到了反射機制

  1. 獲取類的實例 通過構造方法getInstance(靜態變量初始化,屬性賦值,構造方法)
  2. 如果實現了BeanNameAware接口,則用反射注入bean賦值給屬性
  3. 如果實現了BeanFactoryAware接口,則設置 beanFactory
  4. 如果實現了ApplicationContextAware,則設置ApplicationContext
  5. 調用BeanPostProcesser的預先初始化方法
  6. 如果實現了InitializingBean,調用AfterPropertySet方法
  7. 調用定製的 init-method()方法 對應的直接 @PostConstruct
  8. 調用BeanPostProcesser的後置初始化完畢的方法
  • 序列化

fastjson可以參考ObjectDeserializer的幾個實現 JavaBeanDeserializer和ASMJavaBeanDeserializer

動態代理

jdk提供了一個工具類來動態生成一個代理,允許在執行某一個方法時進行額外的處理

java反射真的慢麼?動態代理會創建很多臨時class?

我們分析下這個方法的實現,首先生成的代理對象,需要實現參數裡面聲明的所有接口,接口的實現應給委託給InvocationHandler進行處理,invocationHandler裡面可以根據method聲明判斷是否需要做增強,所以所生成的代理類裡面必須能夠獲取到InvocationHandler,在我們無法知道代理類的具體類型的時候,我們可以通過反射從構造方法裡將InvocationHandler傳給代理類的實例 所以 總的來說生成代理對象需要兩步

  1. 獲取代理類的class對象
  2. 通過class對象獲取構造方法,通過反射生成代理類的實例,並將InvocationHandler傳人
java反射真的慢麼?動態代理會創建很多臨時class?

下面我們在看下 getProxyClass0 如何獲取代理類的class對象,這裡idk通過WeakCache來緩存已經生成的class對象,因為生成該class通過字節碼生成還是很耗時,同時為了解決之前由於動態代理生成太多class對象導致內存不足,所以這裡通過弱引用WeakReference來緩存所生成的代理對象class,當發生GC的時候如果該class對象沒有其他的強引用將會被直接回收 生成代理類的class在ProxyGenerator的generateProxyClass方法內實現,該方法返回一個byte[]數組,最後通過一個本地方法加載到虛擬機,所以可以看出生成該對象還是非常耗時的

java反射真的慢麼?動態代理會創建很多臨時class?

java反射真的慢麼?動態代理會創建很多臨時class?

上面的流程可以簡單歸納為

  1. 增加hashcode,equals,toString方法
  2. 增加所有接口中聲明的未實現方法
  3. 增加一個方法參數為java/lang/reflect/InvocationHandler的構造方法
  4. 其他靜態初始化數據

動態代理的應用

1spring-aop

spring aop默認基於jdk動態代理來實現,我們來看下下面這個經典的面試問題

一個類裡面,兩個方法A和方法B,方法B上有加註解做事物增強,那麼A調用this.B為什麼沒有事物效果?

因為spring-aop默認基於jdk的動態代理實現,最終執行是通過生成的代理對象的,而代理對象執行A方法和B方法其實是調用的InvocationHandler裡面的增強後的方法,其中B方法是經過InvocationHandler做增強在方法前後增加了事物開啟和提交的代碼,而真正執行代碼是通過methodB.invoke(原始對象) 而A方法的實現內部雖然包含了this.B方法 但其實是調用了methodA.invoke(原始對象),而這一句代碼相當於調用的是原始對象的methodA方法,而這裡面的this.B()方法其實是調用的原始對象的B方法,沒有進行過事物增強,而如果是通過cglib做字節碼增強,生成這個類的子類,這種調用this.B方法是有事物效果的

java反射真的慢麼?動態代理會創建很多臨時class?

2rpc consumer

有過RMI開發經驗的人可能會很熟悉,為什麼在對外export rmi服務的時候會分別在client和server生成兩個stub文件,其中client的文件其實就是用動態代理生成了一個代理類 這個代理類,實現了所要對外提供服務的所有接口,每個方法的實現其實就是將接口信息,方法聲明,參數,返回值信息通過網絡發給服務端,而服務端收到請求後通過找到對應的實現然後用反射method.invoke進行調用,然後將結果返回給客戶端

其實其他的RPC框架的實現方式大致和這個類似,只是客戶端的代理類,可能不僅要將方法聲明通過網絡傳輸給服務提供方,也可以做一下服務路由,負載均衡,以及傳輸一些額外的attachment數據給provider

java反射真的慢麼?動態代理會創建很多臨時class?


分享到:


相關文章: