Java反射原理及其性能优化

目录

  1. JVM是如何实现反射的
  2. 反射的性能开销体现在哪里
  3. 如何优化反射性能开销

1. JVM是如何实现反射的?

反射是Java语言中的一种强大功能,它允许程序在运行时动态地获取类的信息以及操作对象。下面是一个简单的示例,演示了如何使用反射调用方法:

复制代码
public class Solution {

    public static void show(int i) {
        new Exception("#" + i).printStackTrace();
    }

    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("Solution");
        Method method = clazz.getMethod("show", int.class);
        method.invoke(null, 0);
    }
}

在上述代码中,我们使用Method.invoke来执行反射方法调用,并通过打印show方法的栈轨迹来观察调用的类。输出如下:

复制代码
java.lang.Exception: #0
    at Solution.show(Solution.java:15)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at Solution.main(Solution.java:21)

首先我们看一下Method.invoke的实现:

复制代码
public final class Method extends Executable {
    ...
    public Object invoke(Object obj, Object... args) throws ... {
        ... // 权限检查
        MethodAccessor ma = methodAccessor;
        if (ma == null) {
            ma = acquireMethodAccessor();
        }
        return ma.invoke(obj, args);
    }
}

可以看到,实际上它是委派给了MethodAccessor来处理。MethodAccessor是一个接口,具有两个具体实现:一个是通过本地方法(NativeMethodAccessorImpl)来实现的,称为本地实现;另一个是使用了委派模式(DelegatingMethodAccessorImpl),称为委派实现。

MethodAccessor实例的创建

MethodAccessor实例是在ReflectionFactory中创建的:

复制代码
public class ReflectionFactory {
    ...
    public MethodAccessor newMethodAccessor(Method method) {
        checkInitted();
        ...
        if (noInflation && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
            return new MethodAccessorGenerator().generateMethod(
                method.getDeclaringClass(),
                method.getName(),
                method.getParameterTypes(),
                method.getReturnType(),
                method.getExceptionTypes(),
                method.getModifiers());
        } else {
            NativeMethodAccessorImpl acc = new NativeMethodAccessorImpl(method);
            DelegatingMethodAccessorImpl res = new DelegatingMethodAccessorImpl(acc);
            acc.setParent(res);
            return res;
        }
    }
}

在第一次调用反射时,noInflationfalse,这时会生成一个委派实现,而委派实现的具体实现便是一个本地实现。反射调用在进入Java虚拟机内部后,实际是调用目标方法的具体地址。

动态生成字节码的实现

Java的反射调用机制还设立了另一种动态生成字节码的实现(简称动态实现),直接使用invoke指令来调用目标方法。动态实现的运行效率要快20倍,因为它避免了Java到C++再到Java的切换,但由于生成字节码非常耗时,仅调用一次的话,本地实现反而要快3到4倍。

Java虚拟机设置了一个阈值15,当某个反射调用的次数在15之下时,采用本地实现;当达到15时,开始动态生成字节码并切换至动态实现,这个过程称为Inflation。

Inflation机制

NativeMethodAccessorImpl中每次invoke方法被调用时,都会增加一次计时器,并判断是否超过阈值,超过后调用MethodAccessorGenerator.generateMethod()生成Java版的MethodAccessor实现类,并改变DelegatingMethodAccessorImpl所引用的MethodAccessor为Java版。

小结

在默认情况下,方法的反射调用为委派实现,调用超过15次后,委派实现便会切换至动态实现,该动态实现的字节码是自动生成的,将直接使用invoke指令调用目标方法。可以通过参数-Dsun.reflect.noInflation=true来关闭Inflation机制,直接生成动态实现。

2. 反射的性能开销体现在哪里?

在上面的例子中,我们使用了Class.forNameClass.getMethod以及Method.invoke三个操作。其中Class.forName调用本地方法,Class.getMethod则遍历该类的公有方法,如果没有匹配到,还将遍历父类的公有方法。这两个操作都是非常耗时的。

需要注意的是,以getMethod为代表的查找方法操作,会返回查找结果的一份拷贝。因此,应避免在热点代码中使用返回Method数组的getMethodsgetDeclaredMethods方法,以减少不必要的堆空间消耗。

在实践中,通常会在应用程序中缓存Class.forNameClass.getMethod的结果,因此下面我们只关注反射调用本身的性能开销。

复制代码
public class ReflectDemo {

    public void doSth(int i) {}

    public static void main(String[] args) throws Exception {
        Class<?> clazz = ReflectDemo.class;
        Constructor constructor = clazz.getConstructor();
        Object object = constructor.newInstance();
        Method method = clazz.getMethod("doSth", int.class);

        ReflectDemo demo = new ReflectDemo();

        long current = System.currentTimeMillis();
        for (int i = 1; i <= 2_000_000_000; i++) {
            if (i % 100_000_000 == 0) {
                long temp = System.currentTimeMillis();
                System.out.println(temp - current);
                current = temp;
            }
            // 直接调用
            demo.doSth(2333);
            // 反射调用
            // method.invoke(object, 2333);
        }
    }
}

根据测试结果,一亿次的直接调用耗时为91.6ms,而反射调用耗时281.6ms,约为基准值的3.07倍。

性能开销来源

反射调用的性能开销主要来自以下几个方面:

  1. 方法表查找
  2. 构建Object数组以及可能存在的自动装拆箱操作
  3. 运行时权限检查
  4. 可能没有方法内联/逃逸分析

3. 如何优化反射性能开销?

反射性能优化策略:

  1. 尽量避免反射调用虚方法:虚方法调用的性能开销更大。
  2. 关闭运行时权限检查 :使用setAccessible(true)可以提升性能。
  3. 扩大基本数据类型对应的包装类缓存 :可通过参数-Djava.lang.Integer.IntegerCache.high=128来实现。
  4. 关闭Inflation机制:直接动态生成字节码。
  5. 提高JVM关于每个调用能够记录的类型数目 :通过虚拟机参数-XX:TypeProfileWidth设置更大的值。

通过上述方法,可以有效减少反射调用的性能开销,提高程序的整体性能。在实际开发中,根据具体场景灵活应用这些优化策略,可以显著提升反射操作的效率。

相关推荐
花椒技术1 天前
直播间常驻子应用加载优化实践:从 1550ms 到 890ms
性能优化·直播·前端工程化
apocelipes2 天前
常用编程语言和库的正则表达式性能对比
c语言·c++·python·性能优化·golang·开发工具和环境
你听得到115 天前
用户说 App 卡,但说不清在哪?我把 Flutter 监控 SDK 升级成了链路观测工作台
前端·flutter·性能优化
亲亲小宝宝鸭9 天前
前端性能监控:web-vitals
前端·性能优化·监控
TrisighT12 天前
Electron 跑在鸿蒙 PC 上,单窗口和多窗口内存差 800MB?我抓了 5 组数据
性能优化·electron·harmonyos
jump_jump16 天前
流式 HTML:从 htmx 片段装配到浏览器原生增量渲染
javascript·性能优化·前端工程化
小小工匠17 天前
Redis - 事务机制:能实现 ACID 属性吗
数据结构·redis·性能优化·并发·持久化
源分享17 天前
Java线程同步的多种实现方法(非常详细)
java·开发语言·jvm
JAVA96517 天前
JAVA面试-JVM篇 03-JVM运行时数据区哪些是线程私有的哪些是共享的
java·jvm·面试
大鱼>17 天前
地平线BPU部署实战:YOLOv8在J5/X3上的算法适配与性能优化
算法·yolo·性能优化