当前位置: 首页 > news >正文

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: #0at 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设置更大的值。

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

http://www.lryc.cn/news/479278.html

相关文章:

  • RabbitMQ 管理平台(控制中心)的介绍
  • 【SQL】在 SQL Server 中创建数据源是 MySQL 数据表的视图
  • 现代Web开发:Next.js 深度解析与最佳实践
  • LeetCode题练习与总结:赎金信--383
  • eval: jdk1.8.0_431/jre/bin/java: Permission denied
  • .Net IOC理解及代码实现
  • 履带机器人(一、STM32控制部分--标准库)
  • 地理空间-Java实现航迹稀释
  • qt QHttpMultiPart详解
  • 【测试】【Debug】vscode中同一个测试用例出现重复
  • Mac上的免费压缩软件-FastZip使用体验实测
  • Linux(CentOS)运行 jar 包
  • 基于YOLOv8 Web的安全帽佩戴识别检测系统的研究和设计,数据集+训练结果+Web源码
  • LabVIEW VISA通信常见问题
  • Node.js Stream(流)以及模块系统使用介绍 (基础介绍 五)
  • 嵌入式linux中设备树控制硬件的方法
  • 定时器入门:Air780E定时器基础与进阶
  • Java LeetCode练习
  • go 集成go-redis 缓存操作
  • python数据结构基础(3)
  • java-智能识别车牌号_基于spring ai和开源国产大模型_qwen vl
  • 全局池化(Global Pooling)
  • ubuntu 24.04运行chattts时cuda安装错误原因分析
  • 使用 Cypher 查询语言在 Neo4j 中查找最短路径
  • Qt多边形填充/不填充绘制
  • 数据结构-数组(稀疏矩阵转置)和广义表
  • Java中的远程方法调用——RPC详解
  • 【kafka】大数据编写kafka命令使用脚本,轻巧简洁实用kafka
  • 交换区(Swap Area或Swap Partition)
  • Excel 无法打开文件