Java类的加载机制

425 阅读7分钟

什么是类加载机制?

代码被编译器编译后成为二进制字节流(.class)文件,JVM把Class文件加载到内存,并进行验证,准备,解析,初始化,最后,能够形成被JVM直接使用的Java类的过程叫做类加载机制。

类加载流程图


类加载机制阶段详解

1.类的加载

类的加载是类加载机制过程的第一个阶段,该阶段主要完成三件任务:

  1. 通过类的权限定名来获取类的二进制字节流。
  2. 将字节流中所有代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中这个类的各种数据的访问入口。

1.1 类的加载器

什么是类的加载器?

把类加载阶段中的通过一个类的全限定名来获取描述此类的二进制字节流这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为类加载器。

1.2类加载器的分类

  • 启动类加载器(Bootstrap ClassLoader)
  • 扩展类加载器(Extension ClassLoader)
  • 应用程序类加载器(Application ClassLoader)
  • 自定义类加载器(USer ClassLoader)

其关系是:

  • 自定义类加载器的父类是应用程序类加载器;
  • 应用程序类加载器的父类是扩展类加载器;
  • 启动类加载器严格意义上不是扩展类加载器的父类,抽象维度可以理解为父类。

1.3类加载器体系结构(双亲委派模型)


类加载器加载过程(双亲委派流程):

类加载器加载过程分为两个环节:缓存查找环节和加载环节

缓存查找环节:

第一步是先检查类加载器中是否已经缓存加载了对应的类。 其中又分为:

① 若存在自定义类加载器,则先检查自身缓存中是否存在;如果存在则取到。

② 如果自定义缓存不存在,委托父类查找,也就是应用程序类加载器。 应用程序类加载器同样也先检查缓存中是否存在,如果存在则取到。

③ 如果应用缓存不存在,则委托它的父类,既是扩展类加载器。 扩展类加载器同样也会先检查缓存中是否存在,如果存在则取到。

④ 如果扩展类加载器缓存也不存在,则调用启动类加载器查找。 启动类加载器也是先检查是否已经加载,如果加载,则取到。如果未加载,则进入加载环节。

加载环节:
第二步,在所有类加载器通过缓存都找不到时,则进入类加载环节。类加载环节可分为:

① 启动类加载器。启动类加载器在缓存找不到后,会根据它的路径范围jre\lib\rt.jar查找加载对应类。如果成功加载,则返回;如果不成功,则进入②。

② 扩展类加载器。扩展类加载器在收到启动类加载器未成功的情况下,会根据它的路径访问jre\lib\ext\*.jar查找加载对应类。如果成功加载,则返回;如果不成功,则进入③。

③ 应用程序类加载器。应用程序类加载器收到扩展类加载器不成功的情况下,会根据它的路劲访问ClassPath查找加载对应的类。如果成功加载,则返回;如果不成功,则进入④。

④ 自定义类加载器。如果应用程序类加载器在收到应用程序类加载器不成功的情况下,会根据它自定义的路径访问查找加载对应的类。如果成功加载,则返回;如果不成功,则抛出ClassNotFoundExcepiton异常。

2.连接

在经历类的加载过程后,生成了类的java.lang.Class对象,接着会进入连接阶段。连接阶段负责将类的二进制数据合并如JRE中。类的连接分为三个阶段。

2.1验证阶段

验证阶段会完成4个验证动作:文件格式验证,元数据验证,字节码验证,符号引用验证。

确保被加载的类符合JVM规范和安全。验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响。

2.2准备阶段

为类的静态变量在方法区上分配内存,并赋上默认初始值(0或者null)。

为类的静态常量在方法区上分配内存,并赋上指定值。

// 静态常量,在准备阶段直接赋值为2
static final int a = 2;
// 静态变量,在准备阶段赋值为0,到了【初始化】阶段赋值为3
static int b = 3;
2.3解析
把类中的符号引用转换为直接引用。
  • 符号引用,就是一组符号来描述目标,可以是任何字面量。
  • 直接引用,就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

2.3.1解析的顺序变化

同常情况下,解析会是在初始化的前一步。但是也会出现在使用之后才会进行解析。

原因是因为:静态解析和动态解析。

静态解析的数据是正常流程,动态解析的数据会出现在使用后在会绑定。

静态解析的范围为:static,construct,final,private

动态解析的范围为:@Override 即重写和实现。在编译期时,存在父类子类方法的重写&接口和实现类对接口方法的实现的相关情况,仅凭静态的代码符号并不能确定关联的类。只有在运行期,JVM能够获取到运行的上下文,才能推断出到底哪个类才是最终要绑定的类,才能获取到最终的直接引用,而确定直接引用的步骤,称之为——解析。

3.初始化

初始化阶段会为类的静态变量赋予正确的初始值。

初始化阶段是执行类构造器<clinit>()方法。

3.1在堆中为静态变量制定初始值有两种方式:

  1. 声明类静态变量是制定初始值。
  2. 使用静态代码块为类静态变量制定初始值。

3.2 JVM初始化步骤

  1. 如果这个类还没有被加载和连接,则程序先进行加载和连接。
  2. 如果这个类的直接父类还没有被初始化,则先初始化其直接父类。
  3. 如果这个类中有初始化语句,则系统会执行一次这些初始化语句。

3.3 类的初始化时机

类的初始化时机,有且只有主动引用时才会触发类的初始化。

3.4 主动引用与被动引用

有且只有主动引用才会触发类初始化的过程。被动引用不会触发类初始化过程。

触发主动引用的五方式:

  1. 创建类的实例。即通过new的方式。
  2. 调用类的静态变量(非final修饰的常量)和静态方法。
  3. 通过反射对类进行调用。(Class.forName()
  4. 初始化某个类的子类,则父类也会被初始化。
  5. Java虚拟机启动时,指定的main方法所在的类,需要被提前初始化。

被动引用的三种方式:

  1. 当访问一个类的静态变量时(该静态变量是父类所持有),只有真正声明这个变量的类才会初始化。子类调用父类的静态变量,只有父类初始化,而子类不会进行初始化。
  2. 通过数据定义引用类,不会触发类的初始化。
  3. final 常量不会触发类的初始化,因为编译阶段就存储在常量池中。

4.使用

5.卸载

类的卸载需要根据该类对象不再被引用+GC回收来判断何时被卸载。

  1. 由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。因为一直被引用着。
  2. 由用户自定义的类加载器加载的类是可以被卸载。