关于java:Java反射源码学习之旅-京东云技术团队

7次阅读

共计 5470 个字符,预计需要花费 14 分钟才能阅读完成。

1 背景

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

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

2 反射包构造梳理

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

① Constructor、Method、Field 三个类型别离能够形容实例的构造方法、一般办法和字段。三种类型都间接或间接继承了 AccessibleObject 这个类型,此类型里次要定义两种办法,一种是通用的、对拜访权限进行解决的办法,第二种是可供继承重写的、与注解相干的办法。

② 只看选中的五种类型,咱们平时所用到的一般类型,譬如 Integer、String,又或者是咱们自定义的类型,都能够用 Class 类型的实例来示意。Java 引入泛型之后,在 JDK1.5 中裁减了其余四种类型,用于泛型的示意。别离是 ParameterizedType(参数化类型)、WildcardType(通配符类型)、TypeVariable(类型变量)、GenericArrayType(泛型数组)。

③ 与②中形容的五种根本类型对应,下图这五个接口 / 类别离用来示意五种根本类型的注解相干数据。

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

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

3 指标类实例的结构源码

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

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

Class 对象的结构由 native 办法实现,以 java.lang.String 类为例,先看看结构好的 Class 对象都有哪些属性:

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

从上图能够看出整个流程有两个外围局部。因为通常状况下,对象的结构都须要依附类里的构造方法来实现,所以第一局部就是拿到指标类对应的 Constructor 对象;第二局部就是利用 Constructor 对象,结构指标类的实例。

3.1 获取 Constructor 对象

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

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 接口的两个实现类,两个对象进行了互相援用,像这样子:

// 结构 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

作者:京东物流 秦曌怡

起源:京东云开发者社区

正文完
 0