松花皮蛋的黑板報
  • 分享在京東工作的技術感悟,還有JAVA技術和業內最佳實踐,大部分都是務實的、能看懂的、可復現的

掃一掃
關注公眾號

JVM是如何實現反射的?

博客首頁文章列表 松花皮蛋me 2019-03-10 16:28

一、反射的基本原理

在Java程序中許多對象在運行時都會有兩種類型:編譯時型、運行時類型,編譯時的類型由聲明時實際的類型決定,運行時的類型由實際覆值給對象的類型決定,如Person p = new Student();編譯時為Person,運行時為Student。程序在運行時還可能接收到外部傳入對象,此對象的編譯類型為Object,但是程序有需要調用此對象運行時類型的方法,為了解決這些問題,程序需要在運行時發現對象和類的真實信息,此時就必須要使用反射了。

也就是說反射是一種特性,它允許正在運行的 Java 程序觀測甚至是修改程序的動態行為。

先看一段代碼

OneClass:

package com.front.ops.soa;
public class One {

private String inner = "inner value";

public String getInner() {
    return inner;
}


public void  call() {
    System.out.println("call run");
}}

ClassTest

package com.front.ops.soa;

import java.lang.reflect.Field;

public class ClassTest {

    private static  Class<One> one = One.class;

    public static void  main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchFieldException {
        One oneObject = one.newInstance();
        oneObject.call();
//                call run
        Field privateField = one.getDeclaredField("inner");
        privateField.setAccessible(true);
        privateField.set(oneObject,"out charge");
        System.out.println(oneObject.getInner());


//        out charge
    }
}

我們可以從上面的代碼中看到Class這個類中之王的強大之處。每個類被加載之后,系統就會為此類生成一個對應的Class對象,通過Class對象就可以訪問到JVM中的這個類,于是就有了通過反射實現AOP編程

二、反射的實現

Method.invoke

public final class Method extends Executable {
  ...
  public Object invoke(Object obj, Object... args) throws ... {
    ... // 權限檢查
    MethodAccessor ma = methodAccessor;
    if (ma == null) {
      ma = acquireMethodAccessor();
    }
    return ma.invoke(obj, args);
  }
}

查閱 Method.invoke 的源代碼,那么就會發現,它實際上委派給 MethodAccessor 來處理。MethodAccessor 是一個接口,它有兩個已有的具體實現:一個通過本地方法來實現反射調用,另一個則使用了委派模式。

每個 Method 實例的第一次反射調用都會生成一個委派實現,它所委派的具體實現便是一個本地實現。本地實現非常容易理解。當進入了 Java 虛擬機內部之后,我們便擁有了 Method 實例所指向方法的具體地址。這時候,反射調用無非就是將傳入的參數準備好,然后調用進入目標方法。

import java.lang.reflect.Method;

public class Test {
  public static void target(int i) {
    new Exception("#" + i).printStackTrace();
  }

  public static void main(String[] args) throws Exception {
    Class<?> klass = Class.forName("Test");
    Method method = klass.getMethod("target", int.class);
    method.invoke(null, 0);
  }
}


$ java Test
java.lang.Exception: #0
        at Test.target(Test.java:5)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl .invoke0(Native Method)
 a      t java.base/jdk.internal.reflect.NativeMethodAccessorImpl. .invoke(NativeMethodAccessorImpl.java:62)
 t       java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.i .invoke(DelegatingMethodAccessorImpl.java:43)
        java.base/java.lang.reflect.Method.invoke(Method.java:564)
  t        Test.main(Test.java:131

可以看到,反射調用先是調用了 Method.invoke,然后進入委派實現(DelegatingMethodAccessorImpl),再然后進入本地實現(NativeMethodAccessorImpl),最后到達目標方法。

其實,Java 的反射調用機制還設立了另一種動態生成字節碼的實現,直接使用 invoke 指令來調用目標方法。之所以采用委派實現,便是為了能夠在本地實現以及動態實現中切換。

// 動態實現的偽代碼,這里只列舉了關鍵的調用邏輯,其實它還包括調用者檢測、參數檢測的字節碼。
package jdk.internal.reflect;

public class GeneratedMethodAccessor1 extends ... {
  @Overrides    
  public Object invoke(Object obj, Object[] args) throws ... {
    Test.target((int) args[0]);
    return null;
  }
}

動態實現和本地實現相比,其運行效率要快上 20 倍 。這是因為動態實現無需經過 Java 到 C++ 再到 Java 的切換,但由于生成字節碼十分耗時,僅調用一次的話,反而是本地實現要快上 3 到 4 倍

在生產環境中,往往擁有多個不同的反射調用,對應多個動態實現,但是可能會造成無法內聯的情況

三、反射的開銷

方法的反射調用會帶來不少性能開銷,原因主要有三個:變長參數方法導致的 Object 數組,基本類型的自動裝箱、拆箱,還有最重要的方法內聯。

1、由于 Method.invoke 是一個變長參數方法,在字節碼層面它的最后一個參數會是 Object 數組。Java 編譯器會在方法調用處生成一個長度為傳入參數數量的 Object 數組,并將傳入參數一一存儲進該數組中。

2、由于 Object 數組不能存儲基本類型,Java 編譯器會對傳入的基本類型參數進行自動裝箱。

3、Class.forName 會調用本地方法,Class.getMethod 則會遍歷該類的公有方法。如果沒有匹配到,它還將遍歷父類的公有方法。可想而知,這兩個操作都非常費時。

四、方法內聯

方法內聯指的是在編譯過程中遇到方法調用時,將目標方法的方法體納入編譯范圍之中,并取代原方法調用的優化手段,采用少干活的方式來提高效率,直接將對應方法的字節碼內聯過來,省下了記錄切換上下文環境的時間和空間

以 getter/setter 為例,如果沒有方法內聯,在調用 getter/setter 時,程序需要保存當前方法的執行位置,創建并壓入用于 getter/setter 的棧幀、訪問字段、彈出棧幀,最后再恢復當前方法的執行。而當內聯了對 getter/setter 的方法調用后,上述操作僅剩字段訪問。

五、逃逸分析

編譯器可以根據逃逸分析的結果進行諸如鎖消除(比如synchronized(new Object()))、棧上分配(方法退出后直接彈出,無須借助垃圾回收器處理)以及標量替換的優化(原本對對象的字段讀取-存儲替換為局部變量的讀取-存儲)

方法內聯失效往往會伴隨著編譯器會認為方法的調用者以及參數是逃逸的,因為對于方法未被內聯的方法調用,即時編譯器會將其當成未知代碼,畢竟它無法確認此方法調用會不會將調用者或所傳入的參數存儲至堆中。

六、反射應用

?-¤?????????alt?±???§??o??o??????????????o7.png


面向AOP切面編程,在傳統的面向對象編程模式中,常見的認證鑒權、日記等等都通過繼承來實現,AOP所提倡的是通過反射或者動態代理加強類的能力來解耦實現,通常運用在權限、緩存、內容傳遞、錯誤處理、懶加載、調試、記錄跟蹤優化校準、性能優化、持久化、資源池、同步、事務等,下面我們來看一個實現多數據庫的例子

定義數據庫枚舉

public enum DataSources {
    DATASOURCE_DEFAULT,
    DATASOURCE_CMDB,
    DATASOURCE_CAP,
    DATASOURCE_MON7, 
    DATASOURCE_OPS,
    DATASOURCE_TIMELINE,
}

定義MyDataSource的注解

@Target({ TYPE, METHOD})
@Retention(RUNTIME)
public @interface MyDataSource {
    DataSources value() default DataSources.DATASOURCE_DEFAULT;
}

我們最終想通過MyDataSource的注解value進行不同Dao的多數據庫支持,實現數據源切換的功能就是自定義一個類擴展AbstractRoutingDataSource抽象類,其實該相當于數據源DataSourcer的路由中介,可以實現在項目運行時根據相應key值切換到對應的數據源DataSource上

public class DataSourceTypeManager {

private static final ThreadLocal<DataSources> dataSourceTypes = new ThreadLocal<DataSources>() {
    @Override
    protected DataSources initialValue() {
        return DataSources.DATASOURCE_DEFAULT;
    }
};

public static DataSources get() {
    return dataSourceTypes.get();
}

public static void set(DataSources dataSourceType) {
    dataSourceTypes.set(dataSourceType);
}

public static void reset() {
    dataSourceTypes.set(DataSources.DATASOURCE_DEFAULT);
}

public static void clearDataSources () {
    dataSourceTypes.remove();
}
}


public class ThreadLocalRountingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceTypeManager.get();
    }
}   

然后聲明切點和方法即可

<bean id="dataSourceAspect" class="com.front.ops.soa.Annotations.DataSourceAspect"/>

<aop:config>
    <aop:aspect ref="dataSourceAspect">
        <aop:pointcut id="dataSourcePointcut" expression="execution(* com.front.ops.soa.Daos.*.*(..) )"/>
        <aop:before pointcut-ref="dataSourcePointcut" method="intercept" />
    </aop:aspect>
</aop:config>
黑龙江6+1开奖结果查询