在讲JDK动态代理之前,我先将反射机制疏离一遍。
该博文非原创,大部分抄录自zhihu用户:bravo1988的原创文章。主要用于本人学习梳理
主要内容
- JVM是如何构建一个实例的
- 类加载器
- Class类
- 反射API
1.JVM是如何构建一个实例的
1.1. 一些名词:
- 内存:即JVM内存,其中被人为的划分为栈,堆,方法区等等
- .class文件:就是字节码文件。
1.2. 粗糙的创建对象过程:
假设main方法中有以下代码:
Person p = new Person();
很多时候我们会认为创建对象的过程如下:
1. 先javac编译源码文件得到.class文件。
2. 再加载.class文件到内存运行。
1.3. 稍微细致一点的创建对象过程
- A a = new A();
- 1.加载类:
ClassLoader类加载器
加载.class文件到呢村
执行静态代码块和静态初始化语句 - 2.执行new,申请一片内存空间
- 3.调用构造器,创建一个空白对象
- 4.子类调用父类构造器。
- 5.构造器执行:执行构造代码块和初始化语句
所以:通过new创建实例和反射创建实例,都绕不开Class对象。
2.类加载器
在上面一节中,了解到.class文件是由类加载器加载的,此处我们先简单的了解一下。
2.1.核心方法loadClass()
- 核心方法loadClass():告诉它需要加载的类名,它会帮你加载
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查是否已经加载该类
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果尚未加载,则遵循父优先的等级加载机制(所谓双亲委派机制)
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) {
// 模板方法模式:如果还是没有加载成功,调用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);
}
2.2.加载.class文件大致可以分为3个步骤:
- 1.检查是否已经加载,有的话就直接返回,避免重复加载
- 2.当前缓存中确实没有该类,那么遵循父优先加载机制,加载.class文件
- 3.如果上面两步都失败了,调用findClass()方法加载。
需要注意的是,ClassLoader类本身是抽象类,而抽象类本身是无法通过new创建对象的。所以它的findClass()方法没有具体的逻辑,留给子类来根据具体情况进行重写。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
即无法通过父类ClassLoader中的findClass()方法加载.class文件。
正确的做法是,子类重写覆盖findClass(),在里面自定义加载逻辑,比如:
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
try {
/*自己另外写一个getClassData()
通过IO流从指定位置读取xxx.class文件得到字节数组*/
byte[] datas = getClassData(name);
if(datas == null) {
throw new ClassNotFoundException("类没有找到:" + name);
}
//调用类加载器本身的defineClass()方法,由字节码得到Class对象
return defineClass(name, datas, 0, datas.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("类找不到:" + name);
}
}
defineClass()是ClassLoader中定义的方法,目的是根据.class文件的字节数组byte[] b造出一个对应的Class对象。底层最终会调用一个native方法:
2.3.反正,目前关于类加载器,只需要大概了如下信息:
- 我们重写了findClass方法:
根据文件系统路劲加载class文件,IO读取.class文件,并返回byte数组
调用ClassLoader提供的方法,将二进制数组转换成Class类的实例对象。 - 现在我们得到了Class类的实例对象。
3.Class类
现在,.class文件已经被加载到内存中了,并且JVM根据其字节数组创建了对应的Class对象。
接下来,我们研究一下Class对象,我们将一步步分析Class类的结构。
- 3.1.Class类中有一个ReflectionData内部类,用于对反射的支持,里面映射着对应.class字节码文件中地属性字段,方法,构造器等信息。
- 3.2.而且为了更详细的描述这些重要的信息,JDK还写了三个类Field,Method,Constructor。这三个类详细的映射了一个类中的属性,方法,构造器的具体信息。而且Class类中的内部类ReflectionData维系着这三个对象的数组形式的引用。
- 3.3.Class类中除了属性,方法,构造器的映射,还有注解,泛型等等这个被映射的类所有的信息。
-
3.4.所以被映射的类A的所有信息,都被“解构”后保存到Class类的实例对象中。其中字段,方法,构造器又用Field,Method,Constructor等类单独映射存储,Class又维系着这三个类的引用属性。
-
3.5.看看Class类的方法:
-
1.构造器:private Class(ClassLoader loader) { }
可以发现,Class类的构造器是私有的,我们无法手动new一个Class对象,只能由JVM创建。
JVM在构造Class对象时,需要传入一个类加载器,然后就是我们上面分析的一连串加载,创建过程。
-
2.Class.forName()方法
静态方法,传入
全类名
,得到该类的Class类对象。底层还是类加载器来做具体实现。
-
3.newInstance()
也就是说,newInstance()底层就是调用无参构造方法来构造一个实例对象。
所以,本质上Class对象要想创建一个它所映射的类的实例,其实都是通过构造器对象的newInstance来实现的。
在此处,如果没有空参构造器对象,就无法使用clazz.newInstance(),必须要获取其他有参的构造器对象,然后调用构造器对象的newInstance()。
4.反射API
日常开发中使用反射的主要目的有两个:
- 创建目标对象的实例:
- 反射调用目标对象中的方法。
创建实例的难点在于,很多人不知道clazz.newInstance()底层还是调用Constructor对象的newInstance()方法,而且这个Constructor对象是一个无参构造器对象。所以要想能够成功创建目标实例,必须保证编写的类有一个无参构造
。
- 4.1.看一下反射调用方法的难点
1.先理清楚Class,Field,Method,Constructor四个对象的关系
-
Class类中有一个内部类ReflectionData,这个内部类中维系着Field,Method,Constructor这三个类的对象数组属性。
-
Field,Method,Constructor对象内部有对字段,方法,构造器更详细的描述:
2.理清楚关系后,我们再来看看反射调用方法时的两个难点。 -
1.难点一:根据Class对象获取Method对象时,需要传入方法名+参数的Class类型。
为什么要传入方法名name+参数的Class类型ParameterType呢?因为目标.class文件中有多个方法,而且每个方法都对应着一个Method对象。那么怎么唯一标识一个方法呢?------
方法签名:方法名称和参数类型
。既然要传参数类型,那么为什么不能直接传入String, int, Integer这些呢?因为这些都是基本类型和引用类型,类型是不能作为参数传递的。我们能传递的要么是值,要么是对象引用。所以要传
参数的Class类型对象
:String.class, int.class。所以必需传入方法名name+参数的Class类型ParameterType。
实际上,调用Class对象的getMethod()方法时,内部会循环遍历所有的Method对象,然后根据方法名和参数类型匹配唯一的一个Method对象返回。循环遍历所有Method,根据name和parameterType匹配
-
2难点二:调用method.invoke(obj, args);时为什么要传入一个目标对象?
上面分析过,.class文件通过IO被加载到内存后,JDK创造了至少四种对象:Class,Field,Method,Constructor。
对象的本质就是用来存储数据的,是类的具体的、单个的具体形象。而方法是一种行为描述,是所有对象共有的,不属于某个对象独有。既然是共性行为,那么可以抽取出来,放在方法区中共用。
既然是共用的,那么怎么确定:要执行的方法是来影响哪个对象呢?所以JVM有一个隐性机制,每次调用非静态方法时,都会隐性传递调用调用该方法的当前对象,称为
隐性参数
。所以方法可以根据这个对象参数知道当前调用本方法的是哪个对象。如果invoke一个静态方法,不需要传入具体的对象。因为静态方法不能处理对象中保存的数据。