Java反射的底层原理,以及Java反射的性能分析及优化

2023-10-27

java的反射技术,号称是编程界的九阳神功,也可以说是框架的灵魂。也正是这种反射机制使静态语言的java具备了动态语言的某些特质。就是有了反射,才让java动态,编程的时候更加灵活,能够动态获取信息以及动态调用对象方法。其实,Java基础技术中的代理,注解也都是依托反射才 能得以实现并应用广泛,另外我们常用的Spring、myBatis等技术框架也都是依托反射才能得以实现。

补充:动态语言 和 静态语言

(1)动态语言

        动态语言是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件改变自身结构。主要动态语言: Object-C.C#、JavaScript, PHP,Python, Erlang..

(2)静态语言

        与动态语言相对应的,运行时结构不可变的语言就是静态语言。如Java.C.C++。

总结:Java不是动态语言,但Java可以称之为“准动态语言”。即Java有一定的动态性,我们可以利用反射机制、字节码操作获得类似动态语言的特性。Java的动态性让编程的时候更加灵活。

如何理解java的反射?反射就是把java的各种成分(字段,方法)映射成相应的java类,比如:Filed类,Method类,Constructor类等,把这些类都封装到一个java类里面,这个java类就是 java.lang.Class<?>类,如下图所示:

1. 那反射到底起了什么作用呢?反射允许程序在运行的时候(注意不是编译的时候),通过Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。例如它允许一个java类去获取它所有的成员和方法并且显示出来。这个其他程序语言所不具备的特质。

疑问:反射机制与面向对象中的封装性是不是矛盾的?如何看待这两个技术?

不矛盾。封装性和反射是两回事。封装性主要是告诉你:没必要再去调private的东西,我有public的方法,已经把功能做的很好了。反射是:我能在运行时去解刨你这个类,并且我还有能力去调用你的private的东西。所有说不矛盾,它是两种思想,反射强调的是动态性(运行时改变程序的结构),封装强调的是保密性和可重复使用性。

扩展下:封装是指隐藏对象的属性和实现细节,仅对外提供公共的访问方式。封装的原则:1. 将不需要对外提供的东西都隐藏起来;2.  把属性都隐藏,但是要提供对应的公共访问方法,你光隐藏而不提供任何访问方法也不行啊。封装的好处:1. 提高安全性;2. 提高重用性;3.方便使用。

疑问:通过直接new的方式或反射的方式都可以调用公共的结构,用哪个更好?

答:静态写代码时,直接用new的方式比较好。程序动态运行时,用反射造对象。

2. 为什么叫反射?说到反射,我们最早接触的反射就是初中物理里面光的反射,光的反射是指光在传播到不同物质时,光在两种物质分界面上改变传播方向又返回原来物质中的现象。光的反射带来的一个现象就是镜面成像。那这个跟我们Java里面的反射有什么关系呢?

要说清这个问题,我们就要从JVM的类加载以及OOP-KLASS模型说起。

JVM 中,使用了 OOP-KLASS 模型来表示 java 对象,即:
2.1. JVM 在加载 class 到内存时,会先解析 class 字节码资源,创建类元数据即 instanceKlass 对象 (C++ 对象) ,包括常量池、字段、方法等,存放在方法区 (hotspot 虚拟机 1.8 以后的版本对应的就是元空间, 1.8 以前的版本就是永久代)
2.2.  new 一个对象时, JVM 会创建 instanceOopDesc 实例,来表示这个对象,存放在堆区,其引用,存放在栈区或堆区;它用来表示对象的实例信息,看起来像个指针,实际上是藏在指针里的对象;instanceOopDesc对应 Java 中的对象实例;
2.3. HotSpot 并不把 instanceKlass 暴露给 Java ,而会另外创建对应的 instanceOopDesc 来表示
java.lang.Class 对象,并将 Class 对象称为 instanceKlass 对象的 “Java 镜像 instanceKlass 对象有一个属性 _java_mirror 指向 Class 对象, klass 持有指向 oop 引用 ( _java_mirror 便是该 instanceKlass Class 对象的引用 )
2.4.  要注意, new 操作返回的 instanceOopDesc 类型有指针指向 instanceKlass ,而 instanceKlass 指向了对 应的类型的Class 实例的 instanceOopDesc ;有点绕,简单说,就是 Person 实例 —>Person 的 instanceKlass—>Person的 Class 。实际上调用 对象 .getClass() 方法时,就是通过 对象先找到
InstanceKlass 对象,然后再通过 _java_mirror 找到 Class 对象的。 instanceKlass 对象和 Class 对象都是描述 class 字节码的数据对象,只不过 instanceKlass 对象在方法区 (Hotspot实现对应是永久代或元空间,取决于 JDK 版本 ) Class 对象在堆区, instanceKlass 对象是 C++ 对 象,Class 对象是 Java 对象 ( 当然物理上是 C++ instanceOopDesc 对象,逻辑上是 Java 对象 ) 。从 Class 对象到Class 对象生成的 Constructor 对象, Method 对象 , Field 对象等,就像物体通过反射在镜子里面生成镜像一样,所以把这种方式称为反射,就很好理解了。
如下图:

综上所述,Class对象存放在方法区吗? 不是的,Class对象实例存放在堆区,存放在方法区的是c++对象,也就是 类元数据即instanceKlass对象。这个Class对象就可以理解为这个c++对象的镜像,本来这个c++对象就有这个镜像属性:_java_mirror。 这个_java_mirror就指向Class对象,其实两个是互指的,你指向我,我也指向你。
所以说我们之前总是说方法区存放了Class实例,其实这样讲不对的,Class实例是存放在堆区的,和我们new出来的java对象一样都是存放在堆区,只是字节码的 instanceKlass 对象也就是C++对象是存放在方法区的。java对象指向C++对象,C++对象再指向Class实例(二者是互指的)。

反射虽然能够使java编程更加灵活,但是它的性能怎么样呢?反射调用方法和直接调用方法这两者之间在性能上面有怎样的差距呢?先说结果吧,如下图:

先解释下这个概念哈:反射调用的时候,每次获取的Method对象,其实它都是返回的一个新对象,这就会出现一个问题,比如循环获取某一Method对象,然后再执行它。这就势必导致很多的新对象产生,这个性能是最慢的。那缓存反射调用是什么意思呢,我只获取一次Method对象,然后缓存起来,最简单的方式就是将获取的Method对象提取到循环的外边,然后循环执行这一个method对象,这个就叫缓存反射调用。

看上图得出的结论:如果不做大的优化的话,如果能缓存Method对象的话,反射耗时大约是直接调用的25倍,如果不能缓存Method对象,例如代理,AOP等场景下,那么反射耗时大约是直接调用的60倍。

3. 那我们来分析下反射为什么这么慢?那我们要先说下java的编译和运行两个阶段。

编译阶段,将.java文件编译成.class文件,编译的时候都是静态的,实例类型,方法名,参数类型等都是确定的,在编译阶段编译器会做权限,可见性,参数等检验。

运行阶段,它首先是包括Java虚拟机加载类的全过程:加载,验证,准备,解析,初始化。包括这5个步骤。验证、准备、解析叫连接过程。

验证是干什么呢,就是验证文件的格式是否正确。

准备是干什么呢,准备就是给类的静态变量赋默认初始化值,注意啊,1. 静态变量,2. 赋值赋的是什么啊,是默认初始化值,它跟变量的真实值没有关系,它只跟变量的类型有关系,比如int类型,那么就赋值0,如果是Integer类型就赋值为null,boolean就赋值为false,它只跟类型有关。想一下:public static int i = 5;  那这个5是什么时候才赋值给 i 的呢? 准备阶段是默认初始化赋值的是0,那5呢,别急,后面还有个初始化阶段,这个5就是初始化阶段给赋值给 i 的。初始化就是给静态变量执行初始化,进行初始化赋值的,还有执行静态代码块,给类进行初始化。

解析是干什么呢?解析的过程会比较复杂,比如:我们的静态main方法加载进内存之后,是不是有一个对应的地址啊,解析就会将main方法这个符号标识替换成对应的内存地址的指针。

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用在class文件中以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现,那解析阶段中,所说的直接引用和符号引用有什么关联呢?
符号引用:符合引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,因为引用的目标不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是他们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在java虚拟机规范的class文件格式中。
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。

(个人理解:解析就是将符号引用变为直接引用。为什么要这么做呢?就是要将对象加载到内存中。“直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄” 这句话意味着对象已经被加载到内存中了。因为句柄的定义就是:当前对象的唯一一个标识。这里说解析阶段的目的是什么。)

回到问题的本质,Java反射性能为什么差?
性能差是相对的,是相对于我们直接调用。

直接调用的时候,是静态的 ,实例类型,方法名,参数类型这些都是明确的,编译阶段已经处理了权限,方法可见性,参数类型等校验,之后jvm加载解析的时候已经将方法的符号引用转为地址引用了,到我们执行方法的时候,就可以直接新建栈帧进行方法调用了。

那么反射的时候就不一样了,反射是动态的,在运行的过程中才知道我们要调用什么类的什么方法,在执行的时候才明确下来,但是你那些编译阶段的校验以及一些安全机制的操作 仍然 是不能少的,所以你在执行的时候依然要做校验等安全机制的操作,所以反射性能慢。另外,它是动态的,所以可能会存在一些jvm无法优化的因素。

分析完java反射的过程以及概述了反射性能慢的原因,下面进行下原因总结:

3.1. 获取Method对象慢:a.  需要检查方法权限;  b. 需要遍历筛选寻找方法,甚至还要遍历父类的方法或者接口;  c, 每一个Method都有一个root,不暴露给外部,而是每次copy一Method。

3.2  调用invoke方法慢: a. invoke调用方法需要对参数做封装和解封装等操作(啥意思?invoke参数是Object,那我传int, long等基本类型的参数,它里面是不是要做封装和拆封的操作?会不会产生大量的对象?);b. 调用的时候还要检查方法的权限,还要校验参数;c, invoke调用逻辑是委托给MethodAccessor的,而这个MethodAccessor对象实懒加载,你第一次调用invoke的时候才创建。

3.3. 因为是动态加载的,vm无法做优化。

补充下:

我们获取Method这个对象,其实它返回方法的拷贝getReflectionFactory().copyMethod()方法。 

里面是这样的:new 一个 Method 实例并返回。

这里有两点要注意:a. 设置 root = this;    b. 会给 Method 设置 MethodAccessor,用于后面方法调用。也就是所有的 Method 的拷贝都会使用同一份 methodAccessor。

介绍下啊,不展示代码了:一共有三种 MethodAccessor。

分别是MethodAccessorImpl,NativeMethodAccessorImpl,DelegatingMethodAccessorImpl

MethodAccessorImpl 是通过动态生成字节码来进行方法调用的,是 Java 版本的 MethodAccessor,字节码生成比较复杂,这里不放代码了。大家感兴趣可以看这里的 generate 方法。

DelegatingMethodAccessorImpl 就是单纯的代理,真正的实现还是 NativeMethodAccessorImpl。

NativeMethodAccessorImpl 是 Native 版本的 MethodAccessor 实现。

采用哪种 MethodAccessor 根据 noInflation 进行判断,noInflation 默认值为 false,只有指定了 sun.reflect.noInflation 属性为 true,才会 采用 MethodAccessorImpl。所以默认会调用 NativeMethodAccessorImpl。

总结下如何去优化选用吧:

1.如果反射调用场景很少,则不需要太过纠结,直接反射调用就行了。如果可以的话,我们可以将Method对象缓存起来,并且设置检查方法的可见性为true:method.setAccessible(true);  首先我们得知道这个反射的代码是没有问题的。

2.如果对性能要求较高,且无法缓存Method对象的情况下,尽量选择AsmReflect来进行反射调用。如果可以缓存,则也可以考虑使用使用Java 版 MethodAccessor,与AsmReflect差异并不是太大。

ReflectAsm优化反射性能,它已经封装好了,它是一个java的字节码操控框架,它可以动态的生成class文件,它这就类似于直接调用了。

这个框架的pom依赖示例如下:

<dependency>

           <groupId>com.esotericsoftware</groupId>
            <artifactId>reflectasm</artifactId>
            <version>1.11.0</version>

</dependency>
 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java反射的底层原理,以及Java反射的性能分析及优化 的相关文章

随机推荐

  • chatglm实现基于知识库问答的应用

    背景 目前由于ChatGPT横空出世 互联网如雨后春笋冒出了非常多的类ChatGPT的大型语言模型 但是对于这些语言模型 我们应该如何将它应用到我们实际的生产中需要一个更加成熟的解决方案 介绍 本文旨在通过介绍ChatGLM的使用来讲述如何
  • Vue 生命周期-钩子函数介绍

    生命周期 每一个组件或者实例都会经历一个完整的生命周期 总共分为三个阶段 初始化 运行中 销毁 钩子函数 vue在整个生命周期里面提供了一些函数 可以在内部实现一些业务逻辑 并且这些函数会在一些特定的场合下去执行 在生命周期的某一个时刻进行
  • 抖音、快手、B站、小红书,品牌如何选对投放平台?

    移动互联网时代 各大平台的品牌广告迎来了爆炸式的增长 穿插在内容流中的信息流广告和KOL内容投放广告对用户来说体验相对较好 对广告主来说可以利用用户的标签进行精准投放 无论是品牌曝光还是获取转化都可满足需求 品牌该如何选择适合自己的平台 让
  • 离散元后处理,将PFC数据写出并导入到matlab中形成云图

    离散元后处理 将PFC数据写出并导入到matlab中形成云图 支持二维三维绘图 内容包括 案例文件 fish代码和matlab代码 pfc2d 3d to matlab ID 71200677036823736
  • 四篇文章带你玩转springboot——3启动源码及外部化配置

    第1章 main入口 public static void main String args 代码很简单SpringApplication run SpringApplication run ConsumerApp class args p
  • Java8常用新特性详解

    文章目录 Java8新特性纵览 Lambda表达式 为什么使用Lambda表达式 简单使用 Lambda语法规则 函数式接口 什么是函数式 Functional 接口 如何理解函数式接口 Java内置函数式接口 自定义函数式接口 方法引用
  • Android中的信息输出:System.out和Log的源码分析与对比(System.out篇)

    想必大家在编写Android程序的时候必然绕不来输出信息 对于熟悉Java的程序员来讲 他们更喜欢使用System out来输出信息 而Android开发经验更丰富的程序员则更倾向于使用Log 毕竟在调试的时候Android环境本身就会输出
  • Cannot read properties of undefined (reading ‘install‘)

    把vue rotuer的版本降低 我降低为了 3 5 4 然后终端重新跑代码
  • 对于开源软件的审查(有无后门等)

    最近公司申请安服资质 让我帮忙写个对开源软件的审查方法 就随便编了以下 如有补充还请各位扶正 其实我觉得他们为啥不怀疑windows有问题呢 非黑客攻击行为安全工具 此类工具通常为辅助测试工具 不具备直接攻击行为 且通常具有官方来源 例如
  • 开源SOLOv2,让动态实例分割更快更强更精准

    作者 介绍 作者建立一个性能强大的简单 通过动态学习对象分割器的蒙版头 使蒙版头以位置为条件 进一步提升性能 具体来说 将遮罩分支解耦为遮罩内核分支和遮罩特征分支 分别负责学习卷积内核和卷积特征 利用矩阵NMS 非最大抑制 以显着减少由于掩
  • 【七夕特辑】用python来实现跳动爱心+爱心心电图+玫瑰花+.....

    前言 什么 明天七夕 学了这么久的python 还写不出来给 对象或者心动Crush 的一个表白代码 还得是我来帮你们准备几个吧 有去年比较火的跳动爱心 也有其他的 来看看 挑选一下 领取代码后 不要忘记发给人家了 emmm先来给你们都看一
  • selenium4 自动化测试--环境安装和基础操作

    最近也有很多人私下问我 selenium学习难吗 基础入门的学习内容很多是3以前的版本资料 对于有基础的人来说 3到4的差别虽然有 但是不足以影响自己 但是对于没有学过的人来说 通过资料再到自己写的代码 发现有些东西没有 有些方法又不相同
  • html给后台发送请求,10分钟学会——前端如何通过AJAX向后端发送请求

    AJAX 基于现有的internet标准 XMLHttpRequest 对象 异步的与服务器交换数据 JavaScript DOM 信息显示 交互 CSS 给数据定义样式 XML 作为转换数据的格式 GET请求 异步请求 来自服务器的响应并
  • 热门前端工具链宣布放弃 TypeScript!

    近日 前端工具链 Turbo 宣布将在 8 0 版本放弃 TypeScript 三个月前 前端框架 Svelte 宣布将在 4 0 版本从 TypeScript 切换到使用 JSDoc 的 JavaScript 如今 又一大前端工具 Tur
  • 安装了pyintaller后出现:‘pyinstaller‘ 不是内部或外部命令,也不是可运行的程序或批处理文件。

    2023年7月31日 周一上午 我昨天晚上也遇到了这个问题 后来解决了 目录 出错原因 解决方法 怎么找到Scripts文件夹 出错原因 出现这个错误是因为你没给python的Scripts文件夹添加环境变量 Scripts存放着pip安装
  • git提交代码

    1 先决条件 代码是repo下来的 代码修改完毕 进入自己修改代码的模块下 git checkout b sprdroid2 3 vlx 3 0 korg 分支名 每个git仓库只用执行一次此命令 以后提交时不用敲此命令 git statu
  • linux中用crontab命令定时执行scrapy项目

    进入文件所在目录 为了保证此方法可行 我先在所在的目录创建一个测试小demo的脚本 想知道如何执行scrapy项目可以直接跳到后面 vi test sh 编写一个每分钟往当前文件中的test txt中写入111的脚本 编写crontab命令
  • Flutter实现圆形头像的几种方法

    Flutter的ClipRect的使用场景 ClipRect可以用来裁剪容器内部的子元素 以避免超出容器的范围而溢出 常见的应用场景有 将一个图像裁剪成不同形状 如圆形 方形 椭圆等 将一个复杂的控件裁剪成更容易理解的形状 裁剪过大的图片或
  • 基于Transformers的自然语言处理入门【十】-机器翻译

    基于Transformers的自然语言处理入门 十 机器翻译 1 机器翻译背景 2 机器翻译模型训练 1 机器翻译背景 机器翻译 是指使用计算机将一种自然语言转换为另一种自然语言的过程 这里 自然语言是指日常使用的人类语言 如中文 英语 区
  • Java反射的底层原理,以及Java反射的性能分析及优化

    java的反射技术 号称是编程界的九阳神功 也可以说是框架的灵魂 也正是这种反射机制使静态语言的java具备了动态语言的某些特质 就是有了反射 才让java动态 编程的时候更加灵活 能够动态获取信息以及动态调用对象方法 其实 Java基础技