反射的基本实现

反射在 Java 中是一种很重要的特性,给 Java 提供了非常方便了的灵活性。它运行正在运行的 Java 程序观察,设置修改程序的动态行为。

举例

  • 可以通过 Class 对象枚举该类的所用方法
  • 通过 Method.setAccessible() 绕过 Java 语言的访问权限。在其他方法中调用该私有方法。
  • Spring 框架中的 IOC 依赖反转基于反射机制。

基本获取 Class 对象方法

  • 使用静态方法 Class.forName 来获取。
  • 调用该对象的 getClass() 对象
  • 类名.class 访问。(基本类型例如 int 可以说 Integer.TYPE,数字可以说 类名[].class )

反射如何调用

常用的方法是调用 Method.invoke ,直接查看源码。

  @CallerSensitive
    public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
               //权限检查
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, obj, modifiers);
            }
        }
               // 委派
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            ma = acquireMethodAccessor();
        }
        return ma.invoke(obj, args);
    }

在前面的部分,是为了判断权限检查的,override 是内部一个遍历,这部分代码是检查 public/private 等权限问题的,不做深究。

后面可以看到,最后会交给 MethodAccessor 来实现,继续进去我们发现它是一个接口。我们再查看它的实现类。

一共有两个:

  • DelegatingMethodAccessorImpl
    • 从字面意思看就知道了,委派实现
  • NativeMethodAccessorImpl
    • Native 是本地的意思,也就是本地方法实现
    • Native 本地方法,它的意思是,由 C++ 代码编写实现的。

使用以下代码,查看以下栈轨迹。

public class Test {
	
	public static void main(String[] args) {
		
		try {
			Class T = Test.class;
			Method method = T.getDeclaredMethod("test1", null);
			method.invoke(T.newInstance(), null);
			System.out.println(method);
			
		} catch (NoSuchMethodException | SecurityException  | IllegalArgumentException | IllegalAccessException | InvocationTargetException | InstantiationException e) {
			e.printStackTrace();
		}
	}
	public void test1() {
		int a = 1 / 0;
	}
	
}

控制台的结果如下:

java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at hello.Test.main(Test.java:17)
Caused by: java.lang.ArithmeticException: / by zero
	at hello.Test.test1(Test.java:26)
	... 5 more

排除除数是 0 异常,我们可以发现,调用顺序是先 Method.invoke - > DelegatingMethodAccessorImpl. invoke(委派实现) - > NativeMethodAccessorImpl.invoke0 (本地实现)最后达到目标方法。

为什么需要委派实现?而不直接调用本地实现。

因为反射调用还有另一种机制,就是动态生成字节码的实现,也就是动态实现。直接使用 invoke 指令来调用目标方法。委派实现在这里的作用就是为了能让本地实现和动态实现直接来回切换。

可以看看 NativeMethodAccessorImpl 的 invoke() 实现。


    public Object invoke(Object obj, Object[] args) 
        throws IllegalArgumentException, InvocationTargetException
    {
        if (++numInvocations > ReflectionFactory.inflationThreshold()) {
            MethodAccessorImpl acc = (MethodAccessorImpl)
                new MethodAccessorGenerator().
                    generateMethod(method.getDeclaringClass(),
                                   method.getName(),
                                   method.getParameterTypes(),
                                   method.getReturnType(),
                                   method.getExceptionTypes(),
                                   method.getModifiers());
            parent.setDelegate(acc);
        }
 
        return invoke0(method, obj, args);

不然发现,当 numInvocations > ReflectionFactory.inflationThreshold(),也就是类膨胀阈值时,MethodAccessorGenerator 会生成一个代理类对象,并且委托给 NativeMethodAccessorImpl 的 parent,也就是 DelegatingMethodAccessorImpl,设置为这个代理类。转为由 GenerateMethodAccessor1 来实现,也就是动态是实现。

其中代理的实现你可以参考下面这段伪代码:

// 这里只列举了关键的调用逻辑,其实它还包括调用者检测、参数检测的字节码。
package jdk.internal.reflect;
 
public class GeneratedMethodAccessor1 extends ... {
  @Overrides    
  public Object invoke(Object obj, Object[] args) throws ... {
    Test.test1();
    return null;
  }
}

比较

动态实现的效率远比本地来得快。因为动态没有 Java 到 c++ 再到 Java 的切换,可是由于生成字节码耗时,所以即调用一次,本地实现会更快。

随意虚拟机设立了一个阈值 15,通过 -Dsun.reflect.inflationThreshold= 就可以调整,在调用次数 15 以下时本地实现,达到 15 时,便开始动态生成字节码,并将委派实现的委派对象切换至动态实现,这个过程我们称之为 Inflation。

将上面调用反射代码修改,让它循环执行 20 次。


public class Test {
	public static void main(String[] args) {
		
		try {
			Class T = Test.class;
			Method method = T.getDeclaredMethod("test1", null);
			for (int i = 0;i < 20; i ++) {
				method.invoke(T.newInstance(), null);
			}
		} catch (NoSuchMethodException | SecurityException  | IllegalArgumentException | IllegalAccessException | InvocationTargetException | InstantiationException e) {
			e.printStackTrace();
		}
	}
	public void test1(int i) {
		new Exception().printStackTrace();
	}
	
}

我们查看控制台的栈轨迹,你可以在 15 次那里发现,进行了转变。

java.lang.Exception
	at hello.Test.test1(Test.java:28)
    // 15 次的时候还是本地方法调用
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at hello.Test.main(Test.java:18)
    
    
java.lang.Exception
	at hello.Test.test1(Test.java:28)
    // 这里改用了 GeneratedMethodAccessor1 动态实现
	at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at hello.Test.main(Test.java:18)

Inflation 机制可以通过参数 -Dsun.reflect.noInflation=true 来关闭的。这样一来,在反射调用一开始便会直接生成动态实现,而不会使用委派实现或者本地实现。

总结

反射在内部有两种实现方式,分别时动态实现和本地实现,本地实现采用的是 C++ 代码实现的,需要 Java 到 C++ 代码的来回转化,而动态实现因为生成字节码的较长,所以在对反射只是调用一两次的情况下,采用本地实现会比动态实现快,而在面对大于 15 次以上(可以调整)的情况下,动态实现反而比本地就更高效率了。

我们还可以把反射的权限的检查关闭(setAccessible(true)) 能够提升一定性能,在 invoke 方法中就体现了。

反射性能的干扰因素

  • 变成参数方法导致的 Object 数组要自动生成
  • 基本类型在传入参数时的自动拆装箱子。
  • 方法的内联。
  • 0
    点赞
  • 5
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

小皮子摘星星

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值