深入学习反射及它的性能分析
前言
先熟悉一下"动态加载":就是在运行的时候才会加载,而不是在编译的时候,在需要的时候才进行加载获取,或者说你可以在任何时候加载一个不存在的类到内存中,然后进行各种交互,或者获取一个没有公开的类的所有信息。也就是说开发者可以随时随意地利用这种机制动态处理一些特殊事务。
反射在平常开发中的应用场景:
- 单纯的反射机制应用框架 例如EventBus
- 逆向代码 ,例如反编译
- 与注解相结合的框架 例如Retrofit
反射的初衷不是方便去创建一个对象,而是让写代码的时候可以更加灵活,降低耦合,提高代码的自适应能力。
如何使用反射
组成是怎样的?
反射需要类参与,一般有以下几个方面:
- java.lang.Class.java:类对象;
- java.lang.reflect.Method.java:类的方法对象;
- java.lang.reflect.Field.java:类的属性对象;
- java.lang.reflect.Constructor.java:类的构造器对象;
反射中类的加载过程:
- 根据JVM工作原理,一般情况下需要经过:加载->验证->准备->解析->初始化->使用->卸载。
- 若需要反射的类没有在内存中,则首先会经过加载这个过程,在内存中生成一个class对象,有这个class对象的引用,就可以处理自己想做的事。
创建对象的流程处理
获取Class对象的几种方式:
- 使用Class类的forName(String clazzName)静态方法。需要传入字符串参数,字符串参数的值是某个类的全限定名(必须添加完整包名)
Class<?> cls=Class.forName("com.demo.Person"); //forName(包名.类名)
Person p= (Person) cls.newInstance();
调用newInstance()方法让加载完的类在内存中创建对应的实例;若找不到类时,会抛出 ClassNotFoundException 异常
- 调用某个类的class属性来获取该类对应的Class对象
Class<?> cls = Person.class;
Person p=(Person)cls.newInstance();
- 调用某个对象的getClass()方法,是java.lang.Object类中的一个方法。
Person p = new Person();
Class<?> cls= p.getClass();
Person p2=(Person)cls.newInstance();
- 1.在内存中新建一个Person的实例,对象p对这个内存地址进行引用
- 2.对象p调用getClass()返回对象p所对应的Class对象
- 3.调用newInstance()方法让Class对象在内存中创建对应的实例,并且让p2引用实例的内存地址
运用反射
- 场景:需要来控制学生、老师或者家长的唱歌行为,可是学生、老师和家长这些类又是由其他人来设计的,你只是对开始与暂停操作进行控制。那么该如何设计?
- 定义Sing 接口
public interface Sing { void start(); // 开始 }
- 动态加载的对象强转为Sing
public class Main { public static void main(String[] args) { try { Sing palyer = (Sing) Class.forName("className").newInstance(); palyer.start(); } catch (Exception e) { e.printStackTrace(); } } }
性能分析
new和Class.forName("").newInstance()创建对象有何区别?
- 在初始化一个类实例时,newInstance()方法和new关键字除了一个是方法,一个是关键字
- 它们创建对象的方式不一样,前者(newInstance)是使用类加载机制,后者(new)是创建一个新类。
- 在使用反射时,必须确保这个类已经加载并连接。使用new时,这个类可以没有被加载,也可以已经被加载。
A a = (A)Class.forName("com.demo.Person").newInstance();
A a = new A();
分析开销的场景
-
反射效率不高,大致有这些原因:
- Method#invoke 方法会对参数做封装和解封操作
- 需要检查方法可见性
- 需要校验参数
- 反射方法难以内联
-
Class.forName方法
- 要调用本地方法,耗时
-
Class.getMethod
- 遍历该类的共有方法,匹配不到时会进而遍历父类共有方法,比较耗时,getMethod会返回得到结果的拷贝,应避免getMethods和getDeclardMethods方法,减少不必要的空间消耗。
-
Method.invoke
- method.invoke(null, i);将invoke的参数改变时,查看字节码可以发现多了新建Object数据和int类型装箱的指令
- Method.invoke是一个变长参数方法,字节码层面它的最后一个参数是object数组,所以编译器会在方法调用处生成一个数据传入;Object数组不能存储基本类型,所以会自动装箱
- 都会带来性能开销,也会占用堆内存,加重gc负担
- 需要检查方法可见性,反射时每次调用都必须检查方法的可见性(Method.invoke 里)
- 需要校验参数,反射时也必须检查每个实际参数与形式参数的类型匹配性(在NativeMethodAccessorImpl.invoke0 里)
如何提高反射效率
-
setAccessible(true)
- 当存在私有变量和方法时,会用到setAccessible(true)方法关闭安全检查。此安全检查其实也是耗时的。
- 在反射的过程中可以尽量调用setAccessible(true)来关闭安全检查,无论是否是私有的,这样也能提高反射的效率。
-
缓存重复用到的对象
- 利用缓存,在平时项目中用到多次的对象会进行缓存,不会多次去创建。在循环时,缓存好实例,就能提高反射的效率,减少耗时。
转载自:https://juejin.cn/post/7249025053475946553