Java的类加载机制及热部署的原理

109 阅读6分钟

什么是类加载

类的加载是指将类的.class二进制数据读入内存,放在运行数据区的方法

类的生命周期

这5个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析的阶段则不一定急的话。

这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交互地混合进行的。

  • 加载

加载阶段虚拟机需要完成一下三件事情

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

相对于其他的阶段,加载阶段是可控性最强的,开发人员可以自定义自己的类加载器

  • 验证

验证是链接的第一阶段,验证阶段非常重要,但不是必须。

如果所引入的类反复验证,可采用 -Xverify:none 来关闭大部分验证,以缩短类加载时间

  • 准备

为静态变量分配内存,初始化默认值。

注意事项

  1. 该阶段进行内存分配的仅包括类变量,而不包括实例变量,实例变量会在对象实例化时候,随对象一块分配在Java堆中

  2. 之类设置的初始值通常是数据类型默认的零值(0,0L,null,false....)

    类变量的定义为

    public static int v =  3;
    

    变量 初始值是0,而不是3。

    v赋值3 是在程序编译后,存放于类的构造器方法中,初始化阶段才会执行类变量的赋值

  3. 如果类字段属性存在Constant,即被final和static同时修饰,那么在准备阶段就会被初始化属性所指定的值

    类变量定义为

    public static final int v = 3;
    

    编译时,javac将会为v赋值为3

  • 初始化

初始化为类的静态变量赋予正确的初始值

JVM初始化步骤

  1. 假设类还没被加载和链接,则程序先加载并链接该类
  2. 假设类的直接父类还没被树池化,则先初始化父类
  3. 假设类中有初始化语句,则系统一次执行这些初始化代码

类的初始化时机

只有当对类的主动使用的时候才会导致类的初始化

类的主动使用包括以下六种

  1. 创建类的实例,也就是new的方式
  2. 访问某个类或接口的静态变量或者对改静态变量赋值
  3. 调用类的静态方法
  4. 反射
  5. 初始化某个类,其父类也会被初始化
  6. Java虚拟机启动时,被标明为启动类的类
  • 结束生命周期

以下情况,Java虚拟机将结束生命周期

  1. 执行System.exit()方法

  2. 程序 正常执行 / 错误异常 / 系统异常 结束

类加载器

类加载器的层次关系图

在这里插入图片描述

注意:这里的父类加载器并不是通过继承的关系来实现的,而是采用组合实现的。

站在Java开发人员的角度,类加载器可划分为以下三类

  1. 启动类加载器[Bootstrap ClassLoader],负责加载。位于JDK\jre\lib目录下,属于JVM虚拟机的一部分。启动类加载器无法被java程序直接引用
  2. 拓展类加载器[Extension ClassLoader],加载器由**sun.misc.Launcher$ExtClassLoader **实现,开发者可以直接使用拓展类加载器
  3. 引用程序加载器[Application ClassLoader],加载器由sun.misc.Launcher$AppClassLoader 实现,负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,一般情况下,这个就是程序中默认的类加载器

Java类加载机制

全盘负责 当一个类加载器加载某个Class时,该Class所 依赖和引用 的其他Class也由该类加载器负责载入,除非显示使用另外一个类加载器

父亲委托 先让父类加载器试图加载该类,当父类加载器无法加载时,尝试从自己的类路径中加载

缓存机制 缓存机制保证所有加载过的Class都会被缓存,当需要某个Class的时候,类加载器先从缓存中寻找该Class,缓存不存在,系统才会读取对应的二进制数据转换成对应的Calss对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM

类的加载

有以下三种方式

  1. 命令行启动,有JVM初始化加载
  2. 通过Class.forName()方法动态加载 [引用程序加载器]
  3. 通过Class.loadCalss()方法动态加载 [引用程序加载器]

示例代码

package com.ys.classLoader;

public class LoadClassTest {
    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = LoadClassTest.class.getClassLoader();
        System.out.println(classLoader);

        // 使用ClassLoader.loadClass()来加载类,不会执行初始化代码
        //classLoader.loadClass("com.ys.classLoader.Test1");

        //使用Class.forName()来加载类,会默认执行初始化代码
        //Class.forName("com.ys.classLoader.Test1");

        //使用Class.forName()来加载类,并指定类加载器,不会执行初始化代码
        Class.forName("com.ys.classLoader.Test1", false, classLoader);
    }
}

Class.forName() 将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;

ClassLoader.loadClass() 只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

补充知识

上面提到newInstance才会去执行,那么 new 关键字 和 newInstance() 方法的区别?

newInstance() 必须保证这个类已经加载并且已经连接

new 关键字 这个类可以没有被加载,不需要该类在classpath中设定,但可能需要通过classlaoder来加载。

双亲委派模型

如果一个类加载起收到类加载的情况,首先不会做自己去尝试加载这个类,而是委托给父加载器去完成,依次向上,最终所有请求都被传递到顶层类加载器。只有父加载器找不到所需要的类时,子加载器才会去加载该类。

传递方式查看类加载器的层次关系图

双亲委派模型的意义

  • 系统类防止内存中出现多分相同的字节码
  • 保证Java程序稳定运行

自定义加载器的应用

  1. 热部署

    JVM有缓存机制,导致修改类,必须重启JVM虚拟机,才会加载新的类。那如何通过我们自己写的类加载器来重新加载改动的类

    通过上面的双亲委派机制得知,如果不打破双亲委派机制的话,自定义类加载器就不会工作。

    具体代码请见: 详解Java的类加载机制

  2. 网络传输的Class加密

    具体请见: 详解Java的类加载机制