JVM类加载原理

198 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情

类的生命周期

一个类从加载到jvm虚拟机到最后的卸载,生命周期总共分为7步:

  1. 加载
  2. 验证
  3. 准备
  4. 解析
  5. 初始化
  6. 使用
  7. 卸载

其中验证、准备、解析统称为链接。加载 验证 准备 初始化和卸载五个阶段顺序是确定的,而解析阶段却不一定,这主要是因为java类加载为懒加载,也就是不会一次性把所有的类全部加载完,有一部分类是在程序运行期间完成的,将符号引用替换为直接引用,也就是动态链接。

jvm类加载过程.png

加载

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

验证

    验证是为了保证被加载类的正确性,确保Class文件的字节码流中包含的信息符合虚拟机的约束要求,是java虚拟机保护自身的一项必要措施。验证主要包含四个动作:

  1. 文件格式验证。验证字节码流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。基于二进制字节流进行,只有通过这阶段的验证,才被允许进入虚拟机内存的方法区进行存储,后续的几个验证阶段也都是基于方法区进行的,不会再重新读取以及操作字节流了,以此可以提升性能。
  2. 元数据验证。对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范
  3. 字节码验证。这个验证阶段是最复杂的一个阶段,主要通过数据流分析和控制流分析,确保程序语义是合法的、符合逻辑的,同时对类的结构体进行校验分析,保证该类的方法在运行时不会做出危害虚拟机安全的行为
  4. 符号引用验证。主要是为了确保解析行为能正常执行,如果校验无法通过,虚拟机将会抛出一个java.lang.IncompatibleClassChangeError的子类异常,eg:java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等

准备

    准备阶段是正式为类中定义的静态变量(被static修饰的变量)分配内存并设置变量初始值的阶段,eg:int赋值为0,boolean赋值为false,对象赋值为null,注意:如果是final修饰符修饰的话即常量,在此阶段会直接赋值。

解析

    解析是将常量池内的符号引用替换为直接引用的过程,该阶段会把一些静态方法(符号引用,比如main())替换为指向数据所在内存的指针或句柄等(即直接引用),这也是常说的静态链接过程(即在类加载期间完成的)

初始化

    此过程对类的静态变量初始化为指定的值,并执行静态代码块。

类加载器

    类加载过程主要通过类加载器实现,java里有以下几种类加载器:

  • 引导类加载器。 BoostrapClassLoader, C语言实现,负责加载支撑JVM运行的位于<JAVA_HOME>/lib目录下的核心类库,比如rt.jar,charsets.jar等
  • 扩展类加载器。ExtClassLoader,主要负责加载支撑JVM运行的位于<JAVA_HOME>/lib/ext目录下的jar包
  • 应用程序类加载器。AppClassLoader, 负责加载classpath路径下类包,主要就是加载自己写的类
  • 自定义类加载器。 负责加载用户自定义路径下的类包,默认父类加载器为应用程序类加载器

    类加载器示例代码:

package com.denticle;
/**
 * Denticle
 * QQ:757005275
 * 类加载器示例代码
 */
public class TestJDKClassLoader {
    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader());
        System.out.println(com.sun.crypto.provider.AESKeyGenerator.class.getClassLoader());
        System.out.println(TestJDKClassLoader.class.getClassLoader());
    }
}

运行结果:

结果.png String.class.getClassLoader()最终结果为null是因为String在rt.jar核心包下,为引导类加载器加载,引导类加载器为C语言实现。

类加载器初始化过程:

    C++调用java代码创建jvm启动器实例sun.misc.Launcher。sun.misc.Launcher初始化使用单例模式设计,保证jvm虚拟机只有一个sun.misc.Launcher实例。在Launcher构造方法内,创建了两个类加载器sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们的应用程序。jdk内源码:

carbon.png

双亲委派机制

    双亲委派的层级结构如下图:

uTools_1664100364754.png

应用程序类加载--->已加载直接返回,否则--->向上委托给父类扩展类加载器加载--->已加载直接返回,否则--->向上委托给父类引导类加载器加载--->已加载直接返回,否则,尝试加载,加载失败--->向下委托给子类扩展类加载器加载--->尝试加载,加载失败--->向下委托给子类应用程序类加载器加载--->尝试加载,加载失败--->未加载到向下委托给自定义类加载器加载--->所有都加载失败抛出ClassNotFoundException异常

注意:此处的父类非java内继承(extends)的意思,这些类加载器并没有继承的关系,只是一个parent属性

双亲委派机制.png

为什么要使用双亲委派机制:

  • 沙箱安全机制: java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
  • 防止类重复加载: Java类随着它的类加载器一起具备了一种带有优先级的层次关系,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次