前言

因为最近在研究Java的反序列化系列漏洞,其中涉及到的一个很重要的部分就是利用反射的方法来调用函数进行代码执行,那么这里就简单的分析下Java反射的流程,但是因为我目前还没有深入学习到Java的内核部分例如JVM以及对于编译原理知识的欠缺,所以这次分析暂时只涉及到Java调用到native方法为止,之后等到我把cool编译器写完,应该就可以进一步深入学习了,这里我们先简单看一下吧。

正文

什么是反射?

在Java运行时环境中,对于任意一个类,可以知道这个类有哪些属性和方法。对于任意一个对象,可以调用它的任意一个方法。这种动态获取类的信息以及动态调用对象的方法的功能来自于Java 语言的反射(Reflection)机制。

为什么需要反射?

在静态语言中,使用一个变量时,必须知道它的类型。在Java中,变量的类型信息在编译时都保存到了class文件中,这样在运行时才能保证准确无误;换句话说,程序在运行时的行为都是固定的。如果想在运行时改变,就需要反射这东西了。

举个简单的例子一个类当需要调用或者另一个类的话,但是另一类可能因为某些原因没有写完,那么如果用通常的方法直接调用是无法通过编译的,那么此时我们就可以利用反射机制,先将需要调用类的名称,方法,参数等都先是写成字符串形式就可以先将这部分编译通过,到了真正运行时再利用反射调用到所需的类。

一个例子

我们可以看到对于反射显示利用.forName方法根据类名获得Class类,然后getMethod根据方法名获得Method类,然后getConstructor获得相应类的Constructor类,再利用newInstance()获得其Object类,最后实际调用invoke方法进行反射的具体执行。

对于反射获得方法的有两种函数

  1. getMethod

    获得所有的public方法,包括父类的,所以说是需要进行递归查询父类方法的,整个的调用流程类似:

    1. getDeclaredMethod

      获得包括public,private,protected方法,但是不包括父类的

invoke源码分析

这里我们还是重点关注invoke的代码调用,我们跟进到invoke函数中

进行权限检查后如果不存在MethodAccessor类,那么新建一个,之后调用MethodAccessor类的invoke方法,我们继续跟进这个invoke

可以看到是一个接口,然后其中有三个实现类,包括DelegatingMethodAccessorImpl,MethodAccessorImpl,NativeMethodAccessorImpl三个实现类,那么就应该根据前面我我们获得MethodAccessor来判断是哪一类

我们跟进下acqurieMethodAccessor()方法,

如果root(在之前getMethod的时候就可能初始了)不为空的话就复用之前的,如果没有责调用reflectionFactory工厂类的newMethodAccessor来获得一个新的MechodAccessor,然后调用setMechodAccessor进行设置,我们继续跟进newMethodAccessor方法

采用哪种 MethodAccessor 根据 noInflation 进行判断,noInflation 默认值为 false,只有指定了 sun.reflect.noInflation 属性为 true,才会采用 MethodAccessorImpl。所以默认会调用 NativeMethodAccessorImpl。

这里的分支应该走else,上面的分支我们之后再说,可以看到调用了NativeMethodAccessorImpl的构造函数,生成了一个 NativeMethodAccessorImpl 对象,再这个对象作为参数调用 DelegatingMethodAccessorImpl 类的构造方法。

然后将这个对象作为参数传递给DelegatingMethodAccessorImpl的构造函数,我们继续跟进这个构造函数

看到其实就是将NativeMethodAccessorImpl对象附值给delegate,也就是说其实DelegatingMethodAccessorImpl就是个代理,类似一个代理模式,综上第一次加载的时候我们应该进入的类DelegatingMethodAccessorImpl,所以我们可以就绪跟进invoke方法了,

调用的还是NativeMethodAccessorImpl的方法,我们继续进行跟进

我们终于快到结束了,我么可以简单研究下这里的逻辑,,有一个 numInvocations 阀值控制,numInvocations 表示调用次数。如果 numInvocations 大于 15(默认阀值是 15),那么就使用 Java 版本的 MethodAccessorImpl,否则使用的是native版本

可以看到当没有超过15次时调用的都是native版本的(c++,c来实现的)

那么为什么会有两个版本呢,注释如下

简单来说就是Java 版本的 MethodAccessorImpl 调用效率比 Native 版本要快 20 倍以上,但是 Java 版本加载时要比 Native 多消耗 3-4 倍资源,所以默认会调用 Native 版本,如果调用次数超过 15 次以后,就会选择运行效率更高的 Java 版本。

但是这又是为什么呢,其中就涉及到jvm以及编译的知识了,这里我还没有学习完,只能给个笼统的理由,摘自网上的blog

是因为是HotSpot的优化方式带来的性能特性,同时也是许多虚拟机的共同点:跨越native边界会对优化有阻碍作用,它就像个黑箱一样让虚拟机难以分析也将其内联,于是运行时间长了之后反而是托管版本的代码更快些。

我的理解可能就是native版本导致jvm无法很好的对代码进行优化。

那么当超过15次后就会调用java版本,生成动态字节码到jvm中

之后的更近一步分析放到之后吧。

关于反射的效率问题

最后我们来谈一谈为什么反射的效率比较慢,其中可能的原因主要有几个

  1. invoke需要对参数进行封装和解封的操作

    invoke调用的参数都是Object类,所以对一些简单类型的参数进行封装的话可能就会造成很多消耗

  2. 对于反射,需要对权限,参数,方法等进行一系列的检查,或者遍历查找都会影响效率

  3. 难以进行内联优化

  4. JIT不好优化(这里还不是特别懂对于JIT)

    反射涉及到动态加载的类型,所以无法进行优化。

总结

如果仅仅是利用提供的api结构,反射确实用起来很简单,但是真正深入进去的话同样也是很复杂,感觉最后都会殊途同归,所以综合来说还是要打好基础啊。