问题
- 1.反射真的慢么?
- 2.动态代理会创建很多临时class?
- 3.属性通过反射读取怎么实现的?
当我们在IDE中编写代码的时候,打一个点号,IDE会自动弹出对应的属性和方法名,当我们在debug的时候,IDE会将方法运行时方法内局部变量和外部实例上属性的值都展示出来,spring中的IOC和AOP,以及一个RPC框架中,我们反序列化,consumer的代理,以及provider的调用都会用到java的反射功能,有人说使用反射会慢,那么到底慢在哪里呢?
反射
反射使JAVA语言有了动态编译的功能,也就是在我们编码的时候不需要知道对象的具体类型,但是在运行期可以通过Class.forName()获取一个类的class对象,在通过newInstance获取实例。
先看下java.lang.reflect包下的几个主要类的关系图,当然动态代理的工具类也在该包下。
AnnotatedElement
作为顶级接口,这个接口提供了获取注解相关的功能,我们在方法,类,属性,构造方法上都可以加注解,所以下面的Field,Method,Constructor都有实现这个接口,以下是我们经常用的两个方法,jdk8以后,接口里面可以通过default修饰方法实现了
- GenericDeclaration
提供了获取泛型相关的功能,只有方法和构造方法上支持泛型,所以只有Method,Constructor实现了该接口
- Member
作为一个对象内部方法和属性的声明的抽象,包含了名称,修饰符,所在的类,其中修饰符包含了 static final public private volatile 等,通过一个整数表示,每一个类型在二进制中占一个位.
AccessibleObject
这是一个类,提供了权限管理的功能,例如是否允许在反射中在外部调用一个private方法,获取一个private属性的值,所以method,constructor,field都继承该类,下面这段代码展示了如何在反射中访问一个私有的成员变量,class对象的构造方法不允许对外.
以下为 Field里面通过field.get(原始对象)获取属性值得实现,先通过override做校验,如果没有重载该权限,则需要校验访问权限
下面我们看看如何通过反射修改Field里面属性的值
通过上面的代码,我们可以看出jdk将Field属性的读取和写入委托给FieldAccessor,那么如何获取FieldAccessor呢
以上代码可以发现,通过工厂模式根据field属性类型以及是否静态来获取,为什么会有这样的划分呢
首先,jdk是通过UNSAFE类对堆内存中对象的属性进行直接的读取和写入,要读取和写入首先需要确定属性所在的位置,也就是相对对象起始位置的偏移量,而静态属性是针对类的不是每个对象实例一份,所以静态属性的偏移量需要单独获取
其实通过该偏移量我们可以大致推断出一个实例内每个属性在堆内存的相对位置,以及分别占用多大的空间,有了位置信息,我们还需要这个字段的类型,以方便执行器知道读几个字节的数据,并且如何进行解析,目前提供了8大基础类型(char vs Charector)和数组和普通引用类型。 java虚拟机为了保证每个对象所占的空间都是8个字节倍数,有时候为了避免两个volatile字段存放在同一个缓存行,所以有时候会再某些字段上做空位填充
以下为UnSafe类的部分代码
}
然后我们在来看看通过反射来调用方法
同样,jdk通过MethodAccessor来进行method的调用,java虚拟机提供了两种模式来支持method的调用 一个是NativeMethodAccessorImpl 一个是通过ASM字节码直接动态生成一个类在invoke方法内部调用目标方法,由于是动态生成所以jdk中没有其源码,但jdk提供了DelegatingMethodAccessorImpl委派模式以方便在运行过程中可以动态切换字节码模式和native模式,我们可以看下生成MethodAccessor的代码
可以看到JDK内部通过numInvocations判断如果该反射调用次数超过ReflectionFactory.inflationThreshold()则用字节码实现,如果小于该值则采用native实现,native的调用比字节码方式慢很多, 动态实现和本地实现相比执行效率要快20倍,因为动态实现无需经过JAVA,C++再到JAVA的转换,之前在jdk6以前有个工具ReflectAsm就是采用这种方式提升执行效率,不过在jdk8以后,也提供了字节码方式,由于许多反射只需要执行一次,然而动态方式生成字节码十分耗时,所以jdk提供了一个阈值默认15,当某个反射的调用次数小于15的话就走本地实现,大于15则走动态模式,而这个阈值可以在jdk启动参数里面做配置
反射为什么慢
经过以上优化,其实反射的效率并不慢,在某些情况下可能达到和直接调用基本相同的效率,但是在首次执行或者没有缓存的情况下还是会有性能上的开销,主要在以下方面
- Class.forName();会调用本地方法,我们用到的method和field都会在此时加载进来,虽然会进行缓存,但是本地方法免不了有JAVA到C+=在到JAVA得转换开销
- class.getMethod(),会遍历该class所有的公用方法,如果没匹配到还会遍历父类的所有方法,并且getMethods()方法会返回结果的一份拷贝,所以该操作不仅消耗CPU还消耗堆内存,在热点代码中应该尽量避免,或者进行缓存
- invoke参数是一个object数组,而object数组不支持java基础类型,而自动装箱也是很耗时的
反射的运用
- spring ioc
spring加载bean的流程基本都用到了反射机制
- 获取类的实例 通过构造方法getInstance(静态变量初始化,属性赋值,构造方法)
- 如果实现了BeanNameAware接口,则用反射注入bean赋值给属性
- 如果实现了BeanFactoryAware接口,则设置 beanFactory
- 如果实现了ApplicationContextAware,则设置ApplicationContext
- 调用BeanPostProcesser的预先初始化方法
- 如果实现了InitializingBean,调用AfterPropertySet方法
- 调用定制的 init-method()方法 对应的直接 @PostConstruct
- 调用BeanPostProcesser的后置初始化完毕的方法
- 序列化
fastjson可以参考ObjectDeserializer的几个实现 JavaBeanDeserializer和ASMJavaBeanDeserializer
动态代理
jdk提供了一个工具类来动态生成一个代理,允许在执行某一个方法时进行额外的处理
我们分析下这个方法的实现,首先生成的代理对象,需要实现参数里面声明的所有接口,接口的实现应给委托给InvocationHandler进行处理,invocationHandler里面可以根据method声明判断是否需要做增强,所以所生成的代理类里面必须能够获取到InvocationHandler,在我们无法知道代理类的具体类型的时候,我们可以通过反射从构造方法里将InvocationHandler传给代理类的实例 所以 总的来说生成代理对象需要两步
- 获取代理类的class对象
- 通过class对象获取构造方法,通过反射生成代理类的实例,并将InvocationHandler传人
下面我们在看下 getProxyClass0 如何获取代理类的class对象,这里idk通过WeakCache来缓存已经生成的class对象,因为生成该class通过字节码生成还是很耗时,同时为了解决之前由于动态代理生成太多class对象导致内存不足,所以这里通过弱引用WeakReference来缓存所生成的代理对象class,当发生GC的时候如果该class对象没有其他的强引用将会被直接回收 生成代理类的class在ProxyGenerator的generateProxyClass方法内实现,该方法返回一个byte[]数组,最后通过一个本地方法加载到虚拟机,所以可以看出生成该对象还是非常耗时的
上面的流程可以简单归纳为
- 增加hashcode,equals,toString方法
- 增加所有接口中声明的未实现方法
- 增加一个方法参数为java/lang/reflect/InvocationHandler的构造方法
- 其他静态初始化数据
动态代理的应用
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方法是有事物效果的
2rpc consumer
有过RMI开发经验的人可能会很熟悉,为什么在对外export rmi服务的时候会分别在client和server生成两个stub文件,其中client的文件其实就是用动态代理生成了一个代理类 这个代理类,实现了所要对外提供服务的所有接口,每个方法的实现其实就是将接口信息,方法声明,参数,返回值信息通过网络发给服务端,而服务端收到请求后通过找到对应的实现然后用反射method.invoke进行调用,然后将结果返回给客户端
其实其他的RPC框架的实现方式大致和这个类似,只是客户端的代理类,可能不仅要将方法声明通过网络传输给服务提供方,也可以做一下服务路由,负载均衡,以及传输一些额外的attachment数据给provider