反射,它就像是一种魔法,引入运行时自省能力,赋予了 Java 语言令人意外的活力,通过运行时操作元数据或对象,Java 可以灵活地操作运行时才能确定的信息

这里笔者就深入浅出总结下Java反射,若有不正确地方,感谢评论区指正交流~ 建议打开idea,写一个Java反射的demo,跟着调试,效果会更好 :)

反射的概念是由Smith在1982年首次提出的,主要是指程序可以访问、检测和修改它本身状态或行为的一种能力。有了反射,使Java相对于C、C++等语言就有了很强大的操作对象属性及其方法的能力,注意,反射与直接调用对象方法和属性相比,性能有一定的损耗,但是如果不是用在对性能有很强的场景下,反射都是一个很好且灵活的选择。

说到反射,首先要了解什么是Class。每个类都会产生一个对应的Class对象,一般保存在.class文件中。所有类都是在对其第一次使用时,动态加载到JVM的,当程序创建一个对类的静态成员的引用时,就会加载这个类。Class对象仅在需要的时候才会加载,static初始化是在类加载时进行的。类加载时,类加载器首先会检查这个类的Class对象是否已被加载过,如果尚未加载,默认的类加载器就会根据类名查找对应的.class文件。

class文件

任何一个Class文件都对应着唯一一个类或接口的信息(这里的类包括抽象类哈),但反过来,类或接口信息并不一定都定义在文件里(比如类或接口可能动态生成,Spring中AOP的实现中就有可能动态生成代理类)。Class文件是一组以8字节为基础单位的二进制文件,各个数据项严格按照顺序紧凑着排列,中间几乎没有任何分隔符,也就是说整个Class文件存储的几乎都是程序运行所需的必要数据。

想在运行时使用类型信息,必须获取对象(比如类Base对象)的Class对象的引用,使用Class.forName(“Base”)可以实现该目的,或者使用Base.class。注意,有一点很有趣,使用”.class”来创建Class对象的引用时,不会自动初始化该Class对应类,使用forName()会自动初始化该Class对应类。使用”.class”不会自动初始化是因为被延迟到了对静态方法(构造器隐私地是静态的)或者非常数静态域进行首次引用时才进行。
为了使用类而做的准备工作一般有以下3个步骤:

  • 加载:由类加载器完成,找到对应的字节码,创建一个Class对象
  • 链接:验证类中的字节码,为静态域分配空间
  • 初始化:如果该类有超类,则对其初始化,执行静态初始化器和静态初始化块

如果不知道某个对象的确切类型,RTTI可以告诉你,但是有一个前提:这个类型在编译时必须已知,这样才能使用RTTI来识别它。而Class类与java.lang.reflect类库一起对反射进行了支持,该类库包含Field、Method和Constructor类,这些类的对象由JVM在启动时创建,用以表示未知类里对应的成员。

反射机制并没有什么神奇之处,当通过反射与一个未知类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪个特定的类。因此,那个类的.class对于JVM来说必须是可获取的,要么在本地机器上,要么从网络获取。所以对于RTTI和反射之间的真正区别只在于:

  • RTTI:编译器在编译时打开和检查.class文件
  • 反射:运行时打开和检查.class文件

反射应用实践

反射获取对象中所有属性值:

public class Person {
private String name;
private int age;
// ...
} Person person = new Person();
person.setName("luo");
person.setAge(25); try {
Class clazz = person.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
System.out.println(field.getType() + " | " + field.getName() + " = " + field.get(person));
} // 通过反射获取某一个方法
Method method = clazz.getMethod("setName", String.class);
method.invoke(person, "bei");
} catch (Exception e) {
e.printStackTrace();
}

平常的项目开发基本很少与反射打交道,因为框架已经帮我们做了很多的事情了。但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架,也是利用CGLIB 反射机制才得以实现。

反射技术在在框架和中间件技术应用较多,有一句老话就是反射是Java框架的基石。典型的使用就是Spring的IoC实现,不管对象谁管理创建,只要我能用就行。再比如RPC技术可以借助于反射来实现,本地主机将要远程调用的对象方法等信息发送给远程主机,这些信息包括class名、方法名、方法参数类型、方法入参等,远程主机接收到这些信息后就可以借助反射来获取并执行对象方法,然后将结果返回即可。

说了那么多,那么Java反射是如何实现的呢?简单来说Java反射就是靠JVM和Class相关类来实现的,Class相关类包括Field、Method和Constructor类等。类加载器加载完成一个类之后,会生成类对应的Class对象、Field对象、Method对象、Constructor对象,这些对象都保存在JVM(方法区)中,这也说明了反射必须在加载类之后进行的原因。使用反射时,其实就是与上述所说的这几个对象打交道呀(貌似Java反射也就这么一回事哈)。

既然了解了Java反射原理,可以试想一下C++为什么没有反射呢,想让C++拥有反射该如何做呢?Java相对于C++实现反射最重要的差别就是Java可以依靠JVM这一悍将,可以由JVM保存对象的相关信息,然后应用程序使用时直接从JVM中获取使用。但是C++编译后直接变成了机器码了,貌似类或者对象的啥信息都没了。。。 其实想让C++有用反射能力,就需要保存能够操作类方法、类构造方法、类属性的这些信息,这些信息要么由应用程序自己来做,要么由第三方工具来保存,然后应用程序使用从它那里获取,这些信息可以通过(函数)指针来记录,使用时通过指针来调用。

反射机制

这里我们以Method.invoke流程来分析反射流程:

public class Person {
private String name;
private int age; public String getName() {
return name;
}
// 其他setter/getter方法
} public static void main(String[] args) throws Exception {
Person person = new Person();
person.setName("luo");
person.setAge(26); for (int i = 0; i < 20; i++) {
Method method = Person.class.getMethod("getName");
System.out.println(method.invoke(person));
}
}

以上代码通过反射调用person对象中的方法,下面跟着源码看下Method.invoke的执行流程:

// Method
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
// 这里会调用reflectionFactory.newMethodAccessor(this)创建一个新的MethodAccessor
// 并赋值给methodAccessor,下次就不会进入到这里了
// ma实际类型是DelegatingMethodAccessorImpl,代理对目标方法的调用
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
} class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
private MethodAccessorImpl delegate;
DelegatingMethodAccessorImpl(MethodAccessorImpl var1) {
this.setDelegate(var1);
} public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
return this.delegate.invoke(var1, var2);
} void setDelegate(MethodAccessorImpl var1) {
this.delegate = var1;
}
} // NativeMethodAccessorImpl
public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
// ReflectionFactory.inflationThreshold()默认15,如果某一个Method反射调用超过15次,
// 则自动生成GeneratedMethodAccessor赋值给DelegatingMethodAccessorImpl.delegate
if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
// 通过asm自动生成MethodAccessorImpl的实现类GeneratedMethodAccessor
MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
this.parent.setDelegate(var3);
} return invoke0(this.method, var1, var2);
}
// native方法,jni方式调用对应方法,调用的是对应的java方法
private static native Object invoke0(Method var0, Object var1, Object[] var2);

下面分别看下NativeMethodAccessorImpl和GeneratedMethodAccessor的调用栈信息:

Java默认在执行Method.invoke超过15次时(通过-Dsun.reflect.inflationThreshold可更改次数值,每个方法对应一个Method对象),JVM会通过asm生成GeneratedMethodAccessor类,由该类调用对应的method方法。执行NativeMethodAccessorImpl.invoke是通过调用JNI方法,在JNI方法中再调用对应的java method方法,这种方式相对于使用GeneratedMethodAccessor.invoe方法来说,前者性能较弱,原因有以下几点:

  • 针对本地方法,jvm无法优化,无法动态inline,其他高级的优化方案都无法优化jni。
  • 执行native涉及到运行栈切换(虚拟机栈切换到本地方法栈),如果本地方法中再调用java方法是有一定的开销的,肯定比不上Java中调用Java方法。
  • 二者内存模型不一样,参数需要转换,比如字符串,数组,复杂结构。转换成本非常高。此开销和调用接口参数有关。

在默认情况下,方法的反射调用为委派实现,委派给本地实现来进行方法调用。在调用超过 15 次之后,委派实现便会将委派对象切换至动态实现。这个动态的字节码是在Java运行过程中通过ASM自动生成的,它将直接使用 invoke 指令来调用目标方法。Java实现的版本在初始化时需要较多时间,但长久来说性能较好;native版本正好相反,启动时相对较快,但运行时间长了之后速度就比不过Java版了。这是HotSpot的优化方式带来的性能特性,同时也是许多虚拟机的共同点:跨越native边界会对优化有阻碍作用,它就像个黑箱一样让虚拟机难以分析也将其内联,于是运行时间长了之后反而是托管版本的代码更快些。

GeneratedMethodAccessor机制

默认method.invoke调用超过15次,会调用MethodAccessorGenerator.generate生成对应GeneratedMethodAccessorN类,代码如下:

// MethodAccessorGenerator
private MagicAccessorImpl generate(final Class<?> var1, String var2, Class<?>[] var3, Class<?> var4, Class<?>[] var5, int var6, boolean var7, boolean var8, Class<?> var9) {
ByteVector var10 = ByteVectorFactory.create();
this.asm = new ClassFileAssembler(var10);
this.declaringClass = var1;
this.parameterTypes = var3;
this.returnType = var4;
this.modifiers = var6;
this.isConstructor = var7;
this.forSerialization = var8;
this.asm.emitMagicAndVersion();
short var11 = 42;
boolean var12 = this.usesPrimitiveTypes();
if (var12) {
var11 = (short)(var11 + 72);
} if (var8) {
var11 = (short)(var11 + 2);
} var11 += (short)(2 * this.numNonPrimitiveParameterTypes());
this.asm.emitShort(add(var11, (short)1));
final String var13 = generateName(var7, var8);
this.asm.emitConstantPoolUTF8(var13);
this.asm.emitConstantPoolClass(this.asm.cpi());
this.thisClass = this.asm.cpi();
if (var7) {
if (var8) {
this.asm.emitConstantPoolUTF8("sun/reflect/SerializationConstructorAccessorImpl");
} else {
this.asm.emitConstantPoolUTF8("sun/reflect/ConstructorAccessorImpl");
}
} else {
this.asm.emitConstantPoolUTF8("sun/reflect/MethodAccessorImpl");
} this.asm.emitConstantPoolClass(this.asm.cpi());
this.superClass = this.asm.cpi();
this.asm.emitConstantPoolUTF8(getClassName(var1, false));
this.asm.emitConstantPoolClass(this.asm.cpi());
this.targetClass = this.asm.cpi();
short var14 = 0;
if (var8) {
this.asm.emitConstantPoolUTF8(getClassName(var9, false));
this.asm.emitConstantPoolClass(this.asm.cpi());
var14 = this.asm.cpi();
} this.asm.emitConstantPoolUTF8(var2);
this.asm.emitConstantPoolUTF8(this.buildInternalSignature());
this.asm.emitConstantPoolNameAndType(sub(this.asm.cpi(), (short)1), this.asm.cpi());
if (this.isInterface()) {
this.asm.emitConstantPoolInterfaceMethodref(this.targetClass, this.asm.cpi());
} else if (var8) {
this.asm.emitConstantPoolMethodref(var14, this.asm.cpi());
} else {
this.asm.emitConstantPoolMethodref(this.targetClass, this.asm.cpi());
} this.targetMethodRef = this.asm.cpi();
if (var7) {
this.asm.emitConstantPoolUTF8("newInstance");
} else {
this.asm.emitConstantPoolUTF8("invoke");
} this.invokeIdx = this.asm.cpi();
if (var7) {
this.asm.emitConstantPoolUTF8("([Ljava/lang/Object;)Ljava/lang/Object;");
} else {
this.asm.emitConstantPoolUTF8("(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;");
} this.invokeDescriptorIdx = this.asm.cpi();
this.nonPrimitiveParametersBaseIdx = add(this.asm.cpi(), (short)2); for(int var15 = 0; var15 < var3.length; ++var15) {
Class var16 = var3[var15];
if (!isPrimitive(var16)) {
this.asm.emitConstantPoolUTF8(getClassName(var16, false));
this.asm.emitConstantPoolClass(this.asm.cpi());
}
} this.emitCommonConstantPoolEntries();
if (var12) {
this.emitBoxingContantPoolEntries();
} if (this.asm.cpi() != var11) {
throw new InternalError("Adjust this code (cpi = " + this.asm.cpi() + ", numCPEntries = " + var11 + ")");
} else {
this.asm.emitShort((short)1);
this.asm.emitShort(this.thisClass);
this.asm.emitShort(this.superClass);
this.asm.emitShort((short)0);
this.asm.emitShort((short)0);
this.asm.emitShort((short)2);
this.emitConstructor();
this.emitInvoke();
this.asm.emitShort((short)0);
var10.trim();
final byte[] var17 = var10.getData(); // class数据
return (MagicAccessorImpl)AccessController.doPrivileged(new PrivilegedAction<MagicAccessorImpl>() {
public MagicAccessorImpl run() {
try {
// 生成反射类GeneratedMethodAccessorN,对应的classLoader为DelegatingClassLoader
return (MagicAccessorImpl)ClassDefiner.defineClass(var13, var17, 0, var17.length, var1.getClassLoader()).newInstance();
} catch (IllegalAccessException | InstantiationException var2) {
throw new InternalError(var2);
}
}
});
}
}

注意:生成对应GeneratedMethodAccessorN类,其对应的classLoader是DelegatingClassLoader,生成的GeneratedMethodAccessorN类是放在永久代的,那么就会产生一个问题,如果数量过多,则会占用永久代太多空间(java8中已没有永久代空间,类数据放在直接内存中)。

生成的GeneratedMethodAccessorN类是什么样的呢?如下所示:

package sun.reflect;

import com.luo.test.InvokeBean;
import java.lang.reflect.InvocationTargetException; public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
public GeneratedMethodAccessor1() {
} public Object invoke(Object var1, Object[] var2) throws InvocationTargetException {
if (var1 == null) {
throw new NullPointerException();
} else {
InvokeBean var10000;
Integer var10001;
try {
var10000 = (InvokeBean)var1;
if (var2.length != 1) {
throw new IllegalArgumentException();
} var10001 = (Integer)var2[0];
} catch (NullPointerException | ClassCastException var4) {
throw new IllegalArgumentException(var4.toString());
} try {
return var10000.test(var10001);
} catch (Throwable var3) {
throw new InvocationTargetException(var3);
}
}
}
}

其实就是直接调用目标对象的具体方法了,和正常的方法调用没太大区别。关于GeneratedMethodAccessorN类加载器更多细节可点击https://www.sohu.com/a/124124072_494943查看~

参考资料:

1、JNI本身会降低效率吗?

2、http://rednaxelafx.iteye.com/blog/548536

3、https://www.sohu.com/a/124124072_494943

深入浅出Java反射的更多相关文章

  1. 第28章 java反射机制

    java反射机制 1.类加载机制 1.1.jvm和类 运行Java程序:java 带有main方法的类名 之后java会启动jvm,并加载字节码(字节码就是一个类在内存空间的状态) 当调用java命令 ...

  2. Java反射机制

    Java反射机制 一:什么事反射机制 简单地说,就是程序运行时能够通过反射的到类的所有信息,只需要获得类名,方法名,属性名. 二:为什么要用反射:     静态编译:在编译时确定类型,绑定对象,即通过 ...

  3. java反射(基础了解)

    package cn.itcast_01; /** *Person类 */ public class Person {    /** 姓名 */    private String name;     ...

  4. java基础知识(十一)java反射机制(上)

    java.lang.Class类详解 java Class类详解 一.class类 Class类是java语言定义的特定类的实现,在java中每个类都有一个相应的Class对象,以便java程序运行时 ...

  5. java基础知识(十一)java反射机制(下)

    1.什么是反射机制? java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象都能够调用他的属性和方法,这种动态获取属性和方法的功能称为java的反射机制. ...

  6. java反射学习之二万能EXCEL导出

    一.EXCEL导出的实现过程 假设有一个对象的集合,现在需要将此集合内的所有对象导出到EXCEL中,对象有N个属性:那么我们实现的方式是这样的: 循环这个集合,在循环集合中某个对象的所有属性,将这个对 ...

  7. java反射学习之一反射机制概述

    一.反射机制背景概述 1.反射(reflection)是java被视为动态语言的一个关键性质 2.反射机制指的是程序在运行时能获取任何类的内部所有信息 二.反射机制实现功能概述 1.只要给定类的全名, ...

  8. java反射 之 反射基础

    一.反射 反射:JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法和属性:这种动态获取的信息以及动态调用对象的方法的功能称为 ...

  9. java反射 cglib asm相关资料

    有篇文章对java反射的调用的效率做了测试,写的比较好.猛击下面地址 http://www.blogjava.net/stone2083/archive/2010/09/15/332065.html ...

随机推荐

  1. Spring Data Redis 让 NoSQL 快如闪电 (1)

    [编者按]本文作者为 Xinyu Liu,详细介绍了 Redis 的特性,并辅之以丰富的用例.在本文的第一部分,将重点概述 Redis 的方方面面.文章系国内 ITOM 管理平台 OneAPM 编译呈 ...

  2. Scala并发编程【进阶】

    package com.dingxin.entrance import java.text.SimpleDateFormat import java.util.Date import scala.ac ...

  3. C#-hello world(二)

     1.C# 程序构成 命名空间(Namespace) 一个 class Class 方法 Class 属性 一个 Main 方法 语句(Statements)和 表达式(Expressions) 注释 ...

  4. SQL Server死锁的解决过程

    某现场报一个SQL死锁,于是开启了1222跟踪: dbcc traceon(1222,-1) 一段时间之后拷贝ERROR文件查找相关信息,比较有用的摘录出来如下: 语句一: select study_ ...

  5. 用beamoff给VMware的Mac OS X 10.10.x加速

    前言 今天刚在VMware里装了个Yosemite,然后测试了下看电影,真j8卡,试了下在vm里打开3d加速,然并卵,直接显示不能打开3d加速,然后找了下发现有个vga的什么软件,是vmware里的显 ...

  6. innerHTML的使用

        inerHTML是html标签的属性,成对出现的标签大多数都有这个属性,用来设置或获取位于对象起始和结束标签 内的HTML.(获取HTML当前标签的起始和结束里面的内容)不包括标签本身.   ...

  7. March 11th, 2018 Week 11th Sunday

    All good things must come to an end. 好景无常. Love is when the other person's happiness is more importa ...

  8. UUID生成随机字符串

    import java.util.UUID; UUID.randomUUID().toString().replace("-", "") 生成的样子      ...

  9. Eclipse的各种查找,类的查找,方法查找快捷键

    eclipse开发中,查找会是一个经常用到的功能所以总结一下1,查找一个类 Shift + Ctrl + h 这种方式能快速的定位接口,类还有注解在那个包里面2.综合查找 Ctrl + H 这是一种综 ...

  10. ES5-ES6-ES7_class类

    传统创建对象模板的方式 JavaScript 语言中,生成实例对象的传统方法是通过构造函数 //JavaScript 语言中,生成实例对象的传统方法是通过构造函数 function Point(x, ...