前语
之前和群友吹水忽然聊到反射,说起第一反应是耗时,但为啥耗时,大脑空空说不上来,为了防止下次面试有人问赶忙测验记录一下,没想到测验成果出人意料。
什么是反射?
反射是一种编程技能,它答应在运转时获取和操作一个程序的元数据(例如类、字段、办法、构造函数等),以及在运转时动态地创立方针、调用办法和拜访成员。
反射是Java独有的特性吗?
除了Java,许多编程言语也支撑类似的反射或元编程特性,答应在运转时获取和操作程序的元数据。以下是一些支撑反射或类似特性的编程言语:
- Python:Python是一种动态言语,它具有强壮的反射和元编程功用。经过运用内置的getattr、setattr、hasattr等函数,开发人员能够在运转时操作方针的属性和办法。
- C#:C#是.NET结构的一部分,它也支撑反射。经过运用System.Reflection命名空间,开发人员能够获取和操作程序集、类型、成员等信息。
- Ruby:Ruby是一种动态言语,具有开放的类结构,答应开发人员在运转时修正类和方针的行为。它提供了Object#send和Object#define_method等办法来实现反射和元编程。
- JavaScript:尽管JavaScript是一种解释性言语,但它也具有一些反射特性。开发人员能够经过Object方针的办法来获取和修正方针的属性和办法。
- PHP:PHP是一种常用于Web开发的脚本言语,它提供了Reflection扩展来支撑反射功用,能够在运转时查看和操作类、办法、属性等信息。
- Kotlin:Kotlin是一种在Java虚拟机上运转的现代编程言语,它也支撑类似于Java的反射功用。经过运用KClass和KFunction等类型,开发人员能够在运转时获取和调用类的信息。
反射的前提条件
运用反射的前提是方针编程言语必须支撑反射机制。反射是一种高档特性,它答应在运转时动态地获取、查看和操作程序的元数据,如类、办法、字段等信息。在运用反射时,需求满意以下前提条件:
- 编程言语支撑反射: 首先,方针编程言语必须具有反射机制或提供相应的库和API,以便在运转时操作程序的结构和元数据。
- 方针元素的可拜访性: 反射答应拜访程序的私有成员和办法,但需求留意的是,拜访私有成员或许违反了封装原则。在运用反射操作私有成员时,需求留意代码的安全性和规划。
- 运转时信息: 反射需求在运转时拜访和操作元数据,因此需求有一个正在运转的程序实例。假如是静态上下文(如在程序未运转时),则无法运用反射。
- 功用和开支: 反射是一种强壮的功用,但在运用时需求留意功用问题。反射操作一般比直接调用更耗费资源,或许会影响程序的功用。因此,在运用反射时应权衡功用和灵活性。
- 对编程言语的了解: 运用反射需求对编程言语的语法、类型体系和元数据有必定的了解。开发人员需求熟悉如何运用反射库或API来获取所需的信息。
反射耗时在哪里
- 反射需求获取类的所有办法,得到一个Method数组,包含着每个办法的参数,返回值类型,权限等信息;
- 需求遍历Method数组,得到咱们需求调用的那个办法,返回其复制,接下来咱们调用其他复制;
- 经过invoke来调用复制的办法,在调用之前,咱们要查看是否有权限履行该办法;
- 调用办法需求对参数进行解封,由于invoke的参数类型是Object,需求将其解封为实践的参数类型;
- 反射需求动态加载,因此无法对其进行及时**(JIT)**优化; 反射的效率丢失首要集中在以上几个方面;
测验反射耗时实战
写一个简略的反射案例和正常调用分别跑1000次看他们的区别
public static void normalExecution(int count) {
long startTime = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
MyClass instance = new MyClass();
instance.setValue(i);
instance.doSomething();
}
long endTime = System.currentTimeMillis();
System.out.println("正常履行循环" + count + "次耗时:" + (endTime - startTime) + "毫秒");
}
public static void reflectionExecutionMethod(int count) {
long startTime = System.currentTimeMillis();
try {
Class<MyClass> clazz = MyClass.class;
Method doSomethingMethod = clazz.getMethod("doSomething");
for (int i = 0; i < count; i++) {
MyClass instance = clazz.newInstance();
doSomethingMethod.invoke(instance);
}
} catch (Exception e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println("运用反射履行 getMethod 循环" + count + "次耗时:" + (endTime - startTime) + "毫秒");
}
public static void reflectionExecutionDeclaredField(int count) {
long startTime = System.currentTimeMillis();
try {
Class<MyClass> clazz = MyClass.class;
Field valueField = clazz.getDeclaredField("value");
valueField.setAccessible(true);
for (int i = 0; i < count; i++) {
MyClass instance = clazz.newInstance();
valueField.set(instance, i);
}
} catch (Exception e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println("运用反射 getDeclaredField 履行循环" + count + "次耗时:" + (endTime - startTime) + "毫秒");
}
static class MyClass {
private int value;
public void doSomething() {
}
public void setValue(int value) {
this.value = value;
}
}
这块代码我分别在编译器和Android虚拟机履行,Android虚拟机**(Pixel 4 XL API 29)** 循环1000次的成果 编译器:getDeclaredField比getMethod快 Android虚拟机:getDeclaredField比getMethod快 循环100000次 编译器:getDeclaredField比getMethod快 Android虚拟机:getMethod比getDeclaredField快 循环1000000次, 编译器:getMethod比getDeclaredField快 Android虚拟:getDeclaredField比getMethod快 当我第一次看到这个成果的时候也是非常不解,连续点了半个小时下来发现成果依然不同,这时我忽然想到是不是编译器的JVM与Android虚拟机的JVM不一样导致的,赶忙查了下资料。
不同的JVM
编译器的JVM(Java Virtual Machine)和Android虚拟机的JVM是两种不同的虚拟机,用于履行Java代码。它们在功用、规划和用处上有一些区别,以下是它们的比较: 编译器的JVM:
- 用处: 编译器的JVM是一般用于在桌面和服务器环境中运转规范的Java应用程序的虚拟机。它履行规范的Java字节码。
- 渠道: 编译器的JVM首要用于支撑规范的**Java SE(Java Standard Edition)**应用程序,能够在不同的操作体系上运转。
- 功用: 提供了规范的Java SE API和功用,包括图形界面、网络通信、多线程等。
- JIT编译: 编译器的JVM一般会运用即时编译**(JIT)**技能,在运转时将字节码编译为本机机器码,以提高履行功用。
Android虚拟机的JVM:
- 用处: Android虚拟机的JVM用于在Android操作体系上运转Android应用程序,它履行的是**Android DEX(Dalvik Executable)字节码,后来转变为ART(Android Runtime)**字节码。
- 渠道: Android虚拟机的JVM是为移动设备和嵌入式体系规划的,首要用于支撑Android应用程序。
- 功用: 提供了Android应用程序所需的功用,如UI渲染、手机硬件拜访、移动网络通信等,同时也支撑规范的Java中心API。
- 运转办法: 在较早的版别中,运用Dalvik虚拟机来解释DEX字节码,而后来的版别中,转为运用ART虚拟机,经过预先编译和优化办法提高履行功用。
个人猜想的JVM功用之差
字节码解释和JIT编译: 在不同的JVM环境下,字节码的解释和JIT编译或许有所不同。在某些情况下,JIT编译器或许会对频繁调用的办法进行优化,使得getMethod在某些情况下履行更快。而getDeclaredField涉及到拜访私有字段而且需求额外的拜访权限,或许在某些情况下履行较慢。 优化战略: 不同的JVM或许有不同的优化战略,例如内联、办法内联等,这些优化战略会影响办法的履行功用。 类加载和初始化: 在不同的环境下,类的加载和初始化次序或许不同,这或许会影响办法调用和字段拜访的功用。 运转时环境: 不同的JVM运转在不同的硬件和操作体系上,硬件和操作体系的差异也会影响功用体现。
结论
- 不要在功用灵敏的应用中,频繁调用反射。
- 假如反射履行的次数小于1000这个数量级,反射的耗时实践上与正常无异。
- 反射对内存占用还有必定影响的,在内存灵敏的场景下,谨慎运用反射。
- 不同的JVM优化战略不同
跋文
上面的测验并不全面,但在必定程度上能够反映出反射的确会导致功用问题,同时不同的JVM优化战略区别。假如后面有必要进一步测验,我会从下面几个方面作进一步测验:
- 测验不同设备调用办法是否会有显着的功用问题;
- 测验同一个办法内,过多的条件判别是否会有显着的功用问题;
- 测验类的杂乱程度是否会对反射的功用有显着影响。
参考
Java反射会影响功用吗?到底慢在哪???_java8 反射功用_sunnylovecmc的博客-CSDN博客