JVM是如何实现反射的?

博客首页文章列表 松花皮蛋me 2019-03-10 08:28
文章首发于公众号 松花皮蛋的黑板报松花皮蛋的黑板报,作者就职于京东,在稳定性保障、敏捷开发、高级JAVA、微服务架构有深入的理解

一、反射的基本原理

在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属性为空;文件名为7.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>