知其然,知其所以然
本文会讲解反射的原理,如果大家对反射不了解,可以先看 《反射从0到入门》 ,对反射有大概的了解。
《反射从入门到精通》我会分为两篇来讲解,这一篇是讲解 Class 类的原理,下一篇我会讲解反射 API 的原理。
孟子曰:得人心者得天下。而在 Java 中,这个「人心」就是 Class 类 ,获取到 Class 类我们就可以为所欲为之为所欲为。下面让我们深入「人心」,去探索 Class 类的原理。
首先了解 JVM 如何构建实例。
JVM:Java Virtual Machine,Java 虚拟机。在 JVM 中分为栈、堆、方法区等,但这些都是 JVM 内存,文中所描述的内存指的就是 JVM 内存。 .class 文件是字节码文件,是通过 .java 文件编译得来的。
知道上面这些内容,我们开始创建实例。我们以创建 Person 对象举例:
Person p = new Person()
简简单单通过 new 就创建了对象,那流程是什么样的呢?见下图
这也太粗糙了一些,那在精致一下吧。
同志们发现没有,其实这里还是有些区别的,我告诉你区别是下面的字比上面多,你会打我不(别打我脸)。
粗糙的那个是通过 new 创建的对象,而精致的是通过 ClassLoader 操作 . class 文件生成 Class 类,然后创建的对象。
其实通过 new 或者反射创建实例,都需要 Class 对象。
.class文件在文章开头讲过,是字节码文件。 .java 是源程序。Java 程序是跨平台的,一次编译到处执行,而编译就是从源文件转换成字节码文件。
字节码无非就是由 0 和 1 构成的文件。
有如下一个类:
通过 vim 查看一下字节码文件:
这啥玩意,看不懂。咱也不需要看懂,反正 JVM 对 .class 文件有它自己的读取规则。
还记得上面的精致图中,我们知道是通过类加载器把 .class 文件加载到内存中。具体的类加载器内容,我会另写一篇文章讲解(写完链接会更新到这里)。但是核心方法就是 loadClass(),只需要告诉它要加载的 name ,它就会帮你加载:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 1.检查类是否已经加载 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { // 2.尚未加载,遵循父优先的等级加载机制(双亲委派机制) if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // 3.如果还没有加载成功,调用 findClass() long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } } // 需要重写该方法,默认就是抛出异常 protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
类加载器加载 .class 文件主要分位三个步骤
因为 ClassLoader 的 findClass 方法默认抛出异常,需要我们写一个子类重新覆盖它,比如:
@Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { // 通过IO流从指定位置读取xxx.class文件得到字节数组 byte[] datas = getClassData(name); if (null == datas){ throw new ClassNotFoundException("类没有找到:" + name); } // 调用类加载器本身的defineClass()方法,由字节码得到 class 对象 return defineClass(name, datas, 0, datas.length); }catch (IOException e){ throw new ClassNotFoundException("类没有找到:" + name); } } private byte[] getClassData(String name) { return byte[] datas; }
defineClass 是通过字节码获取 Class 的方法,是 ClassLoader 定义的。我们具体不知道如何实现的,因为最终会调用一个 native 方法:
private native Class<?> defineClass0(String name, byte[] b, int off, int len, ProtectionDomain pd); private native Class<?> defineClass1(String name, byte[] b, int off, int len, ProtectionDomain pd, String source); private native Class<?> defineClass2(String name, java.nio.ByteBuffer b, int off, int len, ProtectionDomain pd, String source);
总结下类加载器加载 . class 文件的步骤:
.class文件已经被类加载器加载到内存中并生成字节数组, JVM 根据字节数组创建了对应的 Class 对象。
接下来我们来分析下 Class 对象。
我们知道 Java 的对象会有下面的信息:
这些信息在 . class 文件以 0101 表示,最后 JVM 会把 .class 文件的信息通过它的方式保存到 Class 中。
在 Class 中肯定有保存这些信息的字段,我们来看一下:
Class类中用 ReflectionData 里面的字段来与 .class 的内容映射,分别映射了字段、方法、构造器和接口。
通过 annotaionData 映射了注解数据,其它的就不展示了,大家可以自行打开 IDEA 查看下 Class 的源码。
那我们看看 Class 类的方法
Class类的构造器是私有的,只能通过 JVM 创建 Class 对象。所以就有了上面通过类加载器获取 Class 对象的过程。
Class.forName()方法还是通过类加载器获取 Class 对象。
newInstance()的底层是返回无参构造函数。
我们来梳理下前面的知识点:
反射的关键点就是获取 Class 类,那系统是如何获取到 Class 类?
是通过类加载器 ClassLoader 将 .class 文件通过字节数组的方式加载到 JVM 中, JVM 将字节数组转换成 Class 对象。那类加载器是如何加载的呢?
Class类的构造器是私有的,所以需要通过 JVM 获取 Class 。
Class.forName()也是通过类加载器获取的 Class 对象。 newInstance 方法的底层也是返回的无参构造函数。