反射机制(1)--- java类加载机制
前言
反射机制在我们开发中经常会用到,尤其是在各种框架中频繁的使用,但是想要更加深入的理解反射机制,首先要理解类的加载机制,只有理解了类的加载机制,才能更清楚明白的理解反射的来源和原理,否则面对一大堆反射相关的API,只会是一脸蒙圈,即使当下会用了,回头还是会忘记。本篇文章就来探讨下反射机制和类加载机制之间的关系,从而可以对反射有一个更清楚明白的理解,来加深印象。
加载方式
-
静态加载:编译时加载相关的类,如果没有则报错
-
动态加载:运行时加载需要的类,如果运行时用不到该类,即使不存在该类,也不报错,当运行到这个类时,才会报错
类加载时机
- 当创建对象时(new)(静态加载)
- 当子类被加载,父类也会被加载(静态加载)
- 调用类中的静态成员时(静态加载)
- 通过反射(动态加载)
加载过程
首先通过javac编译将java文件编译为class文件。
加载
JVM在该阶段的主要目的是将字节码从不同的数据源(class文件、jar包、网络等)转化为二进制字节流加载到内存中,并生成一个代表该类的java.lang.Class对象
链接
- 验证:目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的额安全,包括文件格式的验证、元数据的验证、字节码验证和符号引用验证
- 准备:JVM会在该阶段对静态变量分配内存并初始化(对应数据类型的默认初始值,如0、false、null等)。这些变量所使用的内存都将在方法区中进行分配。看如下代码示例,可以看到不同类型的变量以及常量在准备阶段的不同处理:
//是实例变量,不是静态变量,因此在准备阶段是不分配内存的
public int a = 0;
//是静态变量,分配内存,默认初始化为0 而不是10
public static int b = 10;
//static final静态常量,和静态变量是不一样的,因为一旦赋值就不变,所以初始化为100
public static final int c = 100;
- 解析:虚拟机将常量池中的符号引用替换为直接引用的过程,符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
初始化
- 到初始化阶段,才真正开始执行类中定义的java程序代码,此阶段是执行< clinit >方法的过程。
- clinit方法是由静态编译器按照语句在源文件中出现的顺序,依次自动收集类中所有静态变量的赋值动作和静态代码块中的语句,并进行合并。
- 如果一个类中没有static的语句块,也没有对static变量的赋值操作,那么虚拟机不会为这个类生成< clinit>()方法。
- 虚拟机会保证一个类中clinit方法在多线程环境下被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的clinit方法,其他线程需要阻塞等待,一直到活动线程执行clinit方法执行完毕
类的加载和反射的关系
我们说的主题是反射,那么为什么要了解类加载的过程呢?我们看一下在加载阶段中,生成了一个代表该类的java.lang.class对象,注意这个对象是在堆中的,跟方法区中的二进制数据不是同一个。我们看一下类经过加载之后,在内存中的布局情况:
这个存在于堆中的Class对象是什么呢?(类的字节码二进制数据存放于方法区,包括方法代码、变量名、方法名、访问权限等等)
- Class也是一个类,是继承自Object
- Class对象不是new出来的,而是由系统创建的
- 对于某个类的class类对象,内存中只有一份,因为类只会被加载一次
- 每个类的实例都会记得自己是由哪个class实例所生成的
- 通过Class对象可以完整的得到一个类的完整结构
要想使用反射,首先需要获取待操作类的对应的Class对象,通过这个对象可以获取整个类的结构,所以java.lang.Class
可以当做是反射的入口点。反射的本质就是在运行时,把java类中的各个成分映射成一个个java对象。
总结
通过以上的分析,我们用图的方式总结一下一个java程序在计算机中的几个阶段:
- 代码编译阶段,可以通过Class.forName()来得到Class对象
- 加载阶段,可以通过类.Class来得到Class
- 运行期间,可以通过对象.getClass() 来获取Class
当我们得到Class对象之后,就可以通过调用一系列的API来操纵java类,可以创建对象,调用对象方法,操作对象属性等等。这一部分放到下一个小节。
转载自:https://juejin.cn/post/6975771347540508709