我的反射测试结果居然与别人不一样

2023-11-22 09:59:13 浏览数 (1)

前言

之前和群友吹水突然聊到反射,说起第一反应是耗时,但为啥耗时,大脑空空说不上来,为了防止下次面试有人问赶紧测试记录一下,没想到测试结果出人意料。

什么是反射?

反射是一种编程技术,它允许在运行时获取和操作一个程序的元数据(例如类、字段、方法、构造函数等),以及在运行时动态地创建对象、调用方法和访问成员。

反射是Java独有的特性吗?

除了Java,许多编程语言也支持类似的反射或元编程特性,允许在运行时获取和操作程序的元数据。以下是一些支持反射或类似特性的编程语言:

  1. Python:Python是一种动态语言,它具有强大的反射和元编程功能。通过使用内置的getattrsetattrhasattr等函数,开发人员可以在运行时操作对象的属性和方法。
  2. C#:C#是.NET框架的一部分,它也支持反射。通过使用System.Reflection命名空间,开发人员可以获取和操作程序集、类型、成员等信息。
  3. Ruby:Ruby是一种动态语言,具有开放的类结构,允许开发人员在运行时修改类和对象的行为。它提供了Object#sendObject#define_method等方法来实现反射和元编程。
  4. JavaScript:虽然JavaScript是一种解释性语言,但它也具有一些反射特性。开发人员可以通过Object对象的方法来获取和修改对象的属性和方法。
  5. PHP:PHP是一种常用于Web开发的脚本语言,它提供了Reflection扩展来支持反射功能,可以在运行时检查和操作类、方法、属性等信息。
  6. Kotlin:Kotlin是一种在Java虚拟机上运行的现代编程语言,它也支持类似于Java的反射功能。通过使用KClassKFunction等类型,开发人员可以在运行时获取和调用类的信息。

反射的前提条件

使用反射的前提是目标编程语言必须支持反射机制。反射是一种高级特性,它允许在运行时动态地获取、检查和操作程序的元数据,如类、方法、字段等信息。在使用反射时,需要满足以下前提条件:

  1. 编程语言支持反射: 首先,目标编程语言必须具有反射机制或提供相应的库和API,以便在运行时操作程序的结构和元数据。
  2. 目标元素的可访问性: 反射允许访问程序的私有成员和方法,但需要注意的是,访问私有成员可能违背了封装原则。在使用反射操作私有成员时,需要注意代码的安全性和设计。
  3. 运行时信息: 反射需要在运行时访问和操作元数据,因此需要有一个正在运行的程序实例。如果是静态上下文(如在程序未运行时),则无法使用反射。
  4. 性能和开销: 反射是一种强大的功能,但在使用时需要注意性能问题。反射操作通常比直接调用更消耗资源,可能会影响程序的性能。因此,在使用反射时应权衡性能和灵活性。
  5. 对编程语言的了解: 使用反射需要对编程语言的语法、类型系统和元数据有一定的了解。开发人员需要熟悉如何使用反射库或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次的结果 编译器:getDeclaredFieldgetMethod

Android虚拟机:getDeclaredFieldgetMethod

循环100000次 编译器:getDeclaredFieldgetMethod

Android虚拟机:getMethodgetDeclaredField

循环1000000次, 编译器:getMethodgetDeclaredField

Android虚拟:getDeclaredFieldgetMethod

当我第一次看到这个结果的时候也是十分不解,连续点了半个小时下来发现结果依然不同,这时我突然想到是不是编译器的JVMAndroid虚拟机的JVM不一样导致的,赶紧查了下资料。

不同的JVM

编译器的JVM(Java Virtual Machine)和Android虚拟机的JVM是两种不同的虚拟机,用于执行Java代码。它们在功能、设计和用途上有一些区别,以下是它们的比较: 编译器的JVM:

  1. 用途: 编译器的JVM是通常用于在桌面和服务器环境中运行标准的Java应用程序的虚拟机。它执行标准的Java字节码。
  2. 平台: 编译器的JVM主要用于支持标准的**Java SE(Java Standard Edition)**应用程序,可以在不同的操作系统上运行。
  3. 功能: 提供了标准的Java SE API和功能,包括图形界面、网络通信、多线程等。
  4. JIT编译: 编译器的JVM通常会使用即时编译**(JIT)**技术,在运行时将字节码编译为本机机器码,以提高执行性能。

Android虚拟机的JVM:

  1. 用途: Android虚拟机的JVM用于在Android操作系统上运行Android应用程序,它执行的是**Android DEX(Dalvik Executable)字节码,后来转变为ART(Android Runtime)**字节码。
  2. 平台: Android虚拟机的JVM是为移动设备和嵌入式系统设计的,主要用于支持Android应用程序。
  3. 功能: 提供了Android应用程序所需的功能,如UI渲染、手机硬件访问、移动网络通信等,同时也支持标准的Java核心API
  4. 运行方式: 在较早的版本中,使用Dalvik虚拟机来解释DEX字节码,而后来的版本中,转为使用ART虚拟机,通过预先编译和优化方式提高执行性能。

个人猜想的JVM性能之差

字节码解释和JIT编译: 在不同的JVM环境下,字节码的解释和JIT编译可能有所不同。在某些情况下,JIT编译器可能会对频繁调用的方法进行优化,使得getMethod在某些情况下执行更快。而getDeclaredField涉及到访问私有字段并且需要额外的访问权限,可能在某些情况下执行较慢。 优化策略: 不同的JVM可能有不同的优化策略,例如内联、方法内联等,这些优化策略会影响方法的执行性能。 类加载和初始化: 在不同的环境下,类的加载和初始化顺序可能不同,这可能会影响方法调用和字段访问的性能。 运行时环境: 不同的JVM运行在不同的硬件和操作系统上,硬件和操作系统的差异也会影响性能表现。

结论

  1. 不要在性能敏感的应用中,频繁调用反射。
  2. 如果反射执行的次数小于1000这个数量级,反射的耗时实际上与正常无异。
  3. 反射对内存占用还有一定影响的,在内存敏感的场景下,谨慎使用反射。
  4. 不同的JVM优化策略不同

后记

上面的测试并不全面,但在一定程度上能够反映出反射的确会导致性能问题,同时不同的JVM优化策略区别。如果后面有必要进一步测试,我会从下面几个方面作进一步测试:

  • 测试不同设备调用方法是否会有明显的性能问题;
  • 测试同一个方法内,过多的条件判断是否会有明显的性能问题;
  • 测试类的复杂程度是否会对反射的性能有明显影响。

参考

Java反射会影响性能吗?到底慢在哪???_java8 反射性能_sunnylovecmc的博客-CSDN博客

0 人点赞