Java反射源码学习之旅

这篇具有很好参考价值的文章主要介绍了Java反射源码学习之旅。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1 背景

前段时间组内针对“拷贝实例属性是应该用BeanUtils.copyProperties()还是MapStruct”这个问题进行了一次激烈的battle。支持MapStruct的同学给出了他嫌弃BeanUtils的理由:因为用了反射,所以慢。

这个理由一下子拉回了我遥远的记忆,在我刚开始了解反射这个Java特性的时候,几乎看到的每一篇文章都会有“Java反射不能频繁使用”、“反射影响性能”之类的话语,当时只是当一个结论记下了这些话,却没有深究过为什么,所以正好借此机会来探究一下Java反射的代码。

2 反射包结构梳理

反射相关的代码主要在jdk rt.jar下的java.lang.reflect包下,还有一些相关类在其他包路径下,这里先按下不表。按照继承和实现的关系先简单划分下java.lang.reflect包:

① Constructor、Method、Field三个类型分别可以描述实例的构造方法、普通方法和字段。三种类型都直接或间接继承了AccessibleObject这个类型,此类型里主要定义两种方法,一种是通用的、对访问权限进行处理的方法,第二种是可供继承重写的、与注解相关的方法。

Java反射源码学习之旅

② 只看选中的五种类型,我们平常所用到的普通类型,譬如Integer、String,又或者是我们自定义的类型,都可以用Class类型的实例来表示。Java引入泛型之后,在JDK1.5中扩充了其他四种类型,用于泛型的表示。分别是ParameterizedType(参数化类型)、WildcardType(通配符类型)、TypeVariable(类型变量)、GenericArrayType(泛型数组)。

Java反射源码学习之旅

③ 与②中描述的五种基本类型对应,下图这五个接口/类分别用来表示五种基本类型的注解相关数据。

Java反射源码学习之旅

④ 下图为实现动态代理的相关类与接口。java.lang.reflect.Proxy主要是利用反射的一些方法获取代理类的类对象,获取其构造方法,由此构造出一个实例。

java.lang.reflect.InvocationHandler是代理类需要实现的接口,由代理类实现接口内的invoke方法,此方法会负责代理流程和被代理流程的执行顺序组织。

Java反射源码学习之旅

Java反射源码学习之旅

3 目标类实例的构造源码

以String类的对象实例化为例,看一下反射是如何进行对象实例化的。

Class<?> clz = Class.forName("java.lang.String");
String s  =(String)clz.newInstance();

Class对象的构造由native方法完成,以java.lang.String类为例,先看看构造好的Class对象都有哪些属性:

Java反射源码学习之旅

可以看到目前只有name一个属性有值,其余属性暂时都是null或者默认值的状态。
下图是 clz.newInstance() 方法逻辑的流程图,接下来对其中主要的两个方法进行说明:

Java反射源码学习之旅

从上图可以看出整个流程有两个核心部分。因为通常情况下,对象的构造都需要依靠类里的构造方法来实现,所以第一部分就是拿到目标类对应的Constructor对象;第二部分就是利用Constructor对象,构造目标类的实例。

3.1 获取Constructor对象

首先上一张Constructor对象的属性图:

Java反射源码学习之旅

java.lang.Class#getConstructor0

此方法中主要做的工作是首先拿到目标类的Constructor实例数组(主要由native方法实现),数组里每一个对象都代表了目标类的一个构造方法。然后对数组进行遍历,根据方法入参提供的parameterTypes,找到符合的Constructor对象,然后重新创造一个Constructor对象,属性值与原Constructor一致(称为副本Constructor),并且副本Constructor的属性 root 指向源Constructor,相当于对源Constructor对象进行了一层封装。

由于在getConstructor0()方法将返回值返回给调用方之后,调用方在后续的流程里进行了constructor.setAccesssible(true)的操作,这个方法的作用是关闭对constructor这个对象访问时的Java语言访问检查。语言访问检查是个耗时的操作,所以合理猜测是为了提高反射性能关闭了这个检查,又出于安全考虑,所以将最原始的对象进行了封装。

private Constructor<T> getConstructor0(Class<?>[] parameterTypes,
                                    int which) throws NoSuchMethodException
{
//1、拿到Constructor实例数组并进行筛选
    Constructor<T>[] constructors = privateGetDeclaredConstructors((which == Member.PUBLIC));
 //2、通过对入参的比较筛选出符合条件的Constructor
    for (Constructor<T> constructor : constructors) {
        if (arrayContentsEq(parameterTypes,
                            constructor.getParameterTypes())) {
//3、创建副本Constructor
            return getReflectionFactory().copyConstructor(constructor);
        }
    }
    throw new NoSuchMethodException(getName() + ".<init>" + argumentTypesToString(parameterTypes));
}

3.2 目标类实例的构造

sun.reflect.ConstructorAccessor#newInstance

此方法主要是利用上一步创建出来的Constructor对象,进行目标类实例的构造。Java为了提高反射的性能,为类实例的构造提供了两种方案,一种是虚拟机自己实现的native方法,一种是JDK包里的Java方法。

首先来看代码里对ConstructorAccessor对象的构造,通过代码可以看出在方法newConstructorAccessor中构造了ConstructorAccessor接口的两个实现类,两个对象进行了相互引用,像这样子:

Java反射源码学习之旅

//构造ConstructorAccessor对象
public ConstructorAccessor newConstructorAccessor(Constructor<?> var1) {
        if (Modifier.isAbstract(var2.getModifiers())) {
      ......
        } else {
            NativeConstructorAccessorImpl var3 = new NativeConstructorAccessorImpl(var1);
            DelegatingConstructorAccessorImpl var4 = new DelegatingConstructorAccessorImpl(var3);
            var3.setParent(var4);
            return var4;
        }
    }

在调用DelegatingConstructorAccessorImpl的newInstance方法时,相当于为NativeConstructorAccessorImpl做了一层代理,实际调用的是NativeConstructorAccessorImpl类实现的方法。

 public Object newInstance(Object[] var1) throws InstantiationException, IllegalArgumentException, InvocationTargetException {
        return this.delegate.newInstance(var1);
    }

newInstance方法中决定使用哪种方法的是一个名为numInvocations的int类型的变量,每次调用到newInstance方法时,这个变量都会+1,当变量值超过阈值(15)时,就会使用Java方式进行目标类实例的创造,反之就会使用虚拟机实现的方式进行目标类实例的创造。

这样做是因为Java版本的实现流程很长,其中还包含了字节码构造的流程,所以初次构造比较耗时,但是长久来说性能更好,而native版本是初期使用速度较块,调用频繁的话性能会有所下降,所以做了根据阈值来判断使用哪个版本的设计。

    public Object newInstance(Object[] var1) throws InstantiationException, IllegalArgumentException, InvocationTargetException {
        if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.c.getDeclaringClass())) {
//Java方法构造对象
            ConstructorAccessorImpl var2 = (ConstructorAccessorImpl)(new MethodAccessorGenerator()).generateConstructor(this.c.getDeclaringClass(), this.c.getParameterTypes(), this.c.getExceptionTypes(), this.c.getModifiers());
            this.parent.setDelegate(var2);
        }
//native方法实现实例化
        return newInstance0(this.c, var1);
    }

重点关注以下Java版本的实现流程,首先构造了一个ConstructorAccessorImpl类的对象。这个对象的构造主要是依靠在代码里按照字节码文件的格式构造出来一个字节数组实现的。首先创建了一个ByteVactor接口的实现类对象,此类有两个属性,一个字节数组,一个int类型的数用来标识位置。ClassFileAssembler类主要负责把各类值转化成字节码的格式然后填充到ByteVactor的实现类对象里。最后由ClassDefiner.defineClass方法对字节码数组进行处理,构造出ConstructorAccessorImpl对象。 最后ConstructorAccessorImpl实例还是会被传给newInstance0()这个native方法,以此来构造最终的目标类实例

private MagicAccessorImpl generate(final Class<?> var1, String var2, Class<?>[] var3, Class<?> var4, Class<?>[] var5, int var6, boolean var7, boolean var8, Class<?> var9) {
 //创建ByteVectorImpl对象
 ByteVector var10 = ByteVectorFactory.create();
 //创建ClassFileAssembler对象
    this.asm = new ClassFileAssembler(var10);
......
        var10.trim();
//拿出构造好的字节数组(就是字节码文件的格式)
        final byte[] var17 = var10.getData();
        return (MagicAccessorImpl)AccessController.doPrivileged(new PrivilegedAction<MagicAccessorImpl>() {
            public MagicAccessorImpl run() {
                try {
//调用native方法,创建ConstructorAccessorImpl类的实例
//最后ConstructorAccessorImpl实例还是会被传给newInstance0()这个native方法,以此来构造最终的目标类实例
                    return (MagicAccessorImpl)ClassDefiner.defineClass(var13, var17, 0, var17.length, var1.getClassLoader()).newInstance();
                } catch (IllegalAccessException | InstantiationException var2) {
                    throw new InternalError(var2);
                }
            }
        });
    }
}

4 小结

最后根据上述学习思考下Java反射到底慢不慢这个问题。首先可以看到JDK为“反射时创建对象的过程”提供了两套实现,native版本更快但是也使得JVM无法对其进行一些优化(譬如JIT的方法内联),当方法成为热点时,转用Java版本来进行实现则优化了这个问题。但Java版本的实现过程中需要动态生成字节码,还要加载一些额外的类,造成了内存的消耗,所以使用反射的时候还是应当注意一些是否会因为使用过多而造成内存溢出。

一次不成熟的源码学习历程,如有错误还请指正。

参考资料:
https://rednaxelafx.iteye.com/blog/548536

作者:京东物流 秦曌怡

来源:京东云开发者社区文章来源地址https://www.toymoban.com/news/detail-508380.html

到了这里,关于Java反射源码学习之旅的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • java学习(二):反射

    https://editor.csdn.net/md/?articleId=131757340 可以参考的笔记: https://blog.csdn.net/ProGram_Java521/article/details/127733167 学习视频: https://www.bilibili.com/video/BV1PY411e7J6?p=192vd_source=585eef59d366645f5bf03840b1010547 https://www.bilibili.com/video/BV1p4411P7V3/?spm_id_from=333.337.search-card.all.click –对程序作出解释。@注

    2024年02月16日
    浏览(32)
  • Java反射机制,动态代理,hook以及在Retrofit源码中的应用

    1.反射的基础知识: Java的反射机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机

    2024年02月13日
    浏览(44)
  • Java学习路线(23)——反射机制

    一、概述 (一)什么是反射: 反射指的是任何一个Class类,在“运行时”都可以直接得到全部成分。 (二)动态可获取的对象: 构造器对象——Constructor,成员变量对象——Field,成员方法对象——Method。 (三)反射关键: 第一步都是得到编译后的Class对象,然后可以获得

    2024年02月08日
    浏览(44)
  • 反射机制-体会反射的动态性案例(尚硅谷Java学习笔记)

    // 举例01 public class Reflect{ } 案例:榨汁机榨水果汁,水果分别有果(com.reflect.Apple)、香蕉(Banana)、桔子(Orange)等。 效果如图。 提示: 1、声明(Fruit)水果接口,包含榨汁抽象方法: void squeeze(); /skwi:z/ 2、声明榨汁机(Juicer),包含运行方法: public void run(Fruit f),方法体中,调用f的榨汁方

    2024年02月11日
    浏览(47)
  • Java基础_反射机制(尚硅谷-学习笔记)

    反射的概述(熟悉) ● Java给我们提供了一套API,使用这套API我们可以在运行时动态的获取指定对象所属的类,创建运行时类的对象,调用指定的结构(属性、方法)等。 ● API: ○ java.lang.Class:代表一个类 ○ java.lang.reflect.Method:代表类的方法 ○ java.lang.reflect.Field:代表类

    2024年02月11日
    浏览(37)
  • Java SE 学习笔记(十七)—— 单元测试、反射

    开发好的系统中存在很多方法,如何对这些方法进行测试? 以前我们都是将代码全部写完再进行测试。其实这样并不是很好。在以后工作的时候,都是写完一部分代码,就测试一部分。这样,代码中的问题可以得到及时修复。也避免了由于代码过多,从而无法准确定位到错误

    2024年02月06日
    浏览(54)
  • 【从零开始学习JAVA | 第四十五篇】反射

    目录 前言: ​反射:  使用反射的步骤: 1.获取阶段: 2.使用阶段: 反射的应用场景: 使用反射的优缺点: 总结: Java中的反射是一项强大而灵活的功能,它允许程序在运行时 动态地获取、操作和利用类的信息 。通过反射,我们可以在运行时检查和修改类的属性、调用类

    2024年02月13日
    浏览(63)
  • re:从0开始的CSS之旅 19. 背景

    background-color 设置背景颜色 transparent 透明的(默认值) background-image 设置背景图片 可选值: none 无背景图片(默认值) url() 背景图片路径 backgroung-repeat 设置背景平铺 可选值: repeat 平铺(默认值) no-repeat 不平铺 repeat-x 水平平铺 repeat-y 垂直平铺 background-position 设置背景图像

    2024年02月19日
    浏览(35)
  • 从零开始学习 Java:简单易懂的入门指南之反射(三十八)

    ​ 专业的解释: ​ 是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法; ​ 对于任意一个对象,都能够调用它的任意属性和方法; ​ 这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。 ​ 通俗的理解: 利用 反射 创建的对象 可

    2024年02月08日
    浏览(49)
  • 解决Java后端开发过程中,后端是BigDecimal类型,返回前段后两位小数是0的话自动去掉的问题

    在Java 后端开发过程中,有的码友们会遇到,当某个价格字段是BigDecimal类型时,值的小数点后面两位0的时候,比喻89.00,返回给前端展示的时候,变成了89,后面的两个小数就不见了。这种情况可以使用自定义序列化方式解决。 第一步,自定义序列化类 在需要序列化的字段上

    2024年02月15日
    浏览(61)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包