前言
之前和群友吹水突然聊到反射,说起第一反应是耗时,但为啥耗时,大脑空空说不上来,为了防止下次面试有人问赶紧测试记录一下,没想到测试结果出人意料。
什么是反射?
反射是一种编程技术,它允许在运行时获取和操作一个程序的元数据(例如类、字段、方法、构造函数等),以及在运行时动态地创建对象、调用方法和访问成员。
反射是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次看他们的区别
代码语言:javascript复制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博客