JVM类加载机制

类加载系统

JVM核心的结构里面有一个就是 类加载系统。 一个java代码经过编译器后变成class字节码文件,后续在运行时,字节码需要通过JVM的类加载系统执行运行。

类加载系统

  • 类的加载过程
  • 类的生命周期
  • 类加载器种类
  • 类加载机制

类生命周期

类的生命周期包括: 加载、链接(验证、准备、解析)、初始化、使用和卸载,

  • 加载

    • 通过类全限定名来获取二进制字节流、
    • 字节流描述的静态存储结构转换为方法区的运行时数据结构
    • Heap中生成一个代表这个类的Class对象,作为方法区访问这些数据的入口
  • 链接

    • 验证 确保被加载的类的正确性

      • 文件格式验证,字节流是否符合Class文件格式,如0xCAFFBABE开头、版本号、常量类型等等
      • 元数据验证, 多字节码描述性信息进行语义分析、确保符合规范
      • 字节码验证
      • 符号引用验证
    • 准备 为类的 静态变量 分配内存,并将其初始化成默认值

    • 解析 把类中的符号引用转换为直接引用(直接指向目标的指针、相对偏移量或间接定位到目标的句柄), 主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符等符号引用进行。

类加载过程

加载、链接(验证、准备、解析)、初始化 属于 类加载的过程

类加载过程

类加载过程从上图可看见

  • 第一个阶段 Loading

通过类的全限定名获取到该类的class二进制字节码流数据,将二进制流数据代表的静态存储结构转换成方法区运行时的数据结构

  • 第二个阶段 Linking
    • 验证 Verify: 保证class文件字节流是符合JVM的要求,确保安全
    • 准备 Prepare: 为静态字段分配内存并设置初始化默认值(被final修饰的static字段不会设置,在编译阶段就分配了)
    • 解析 Resolve: 解析是为了将常量迟的符号引用转换为直接引用(实际引用),如果符号引用指向未被加载的类,或未被加载的类的字段或方法,那么解析触发了这个类的加载(但是不一定触发这个类的LinkingInitialization
  • 第三个阶段 Initialization

初始化阶段是执行类的构造方法,init() 的过程,(编译器自动收集类的所有变量赋值动作和静态代码块语句合并的) 若该类有父类,JVM保证父类先执行init然后执行子类的init

类加载器的分类

上面描述了类加载的机制,那么用于类加载的组件我们叫类加载器,而且类加载器是有分类的

  • Bootstrap ClassLoader

启动类加载器,是JVM内部实现的,Java语言程序无法直接操作,主要用来加载Java核心类库,诸如rt.jarresources.jarsun.boot.class.path目录下的包,用于JVM运行所需的包(一般只加载 包名为javajavaxsun开头的类)

它加载Extension ClassLoaderApplication ClassLoader,并成为它们的父类加载器

  • Extension ClassLoader

扩展类加载器(sun.misc.Launcher$ExtClassLoader实现), 派生继承 java.lang.ClassLoader, 父类加载器是 启动类加载器, 加载 lib/ext 目录下的加载类库。可以将自己的包放到该目录下,就会自动加载进入

  • Application ClassLoader

应用程序类加载器(sun.misc.Launcher$AppClassLoader实现), 派生继承 java.lang.ClassLoader, 父类加载器是 启动类加载器,加载 classpath 或系统 java.class.path 路径洗的类库

程序的默认的类加载器,Java程序中的类,由它加载完成

  • … (自定义类加载器)

在开发过程中,可能需要使用到一些操作,可以自定义类加载器,达到一些方式,比如应用程序的启动自动激活检查等等,或者密钥的验证等等。 可以开发自定义的网络加载 Java 类, 并进行加解密操作

实现自定义类加载器的过程

  • 继承 java.lang.ClassLoader,重写 findClass() 方法
  • 也可以继承 URLClassLoader类,重写 loadClass() 方法(可以参考 AppClassLoaderExtClassLoader

类加载机制

JVM 对 class文件 采用的是 按需加载 方式,只有需要使用到这个类的时候,JVM才会将其 class文件加载到内存中并产生 class对象

采用的机制: 双亲委派机制,即把加载请求交给父类处理的一直任务模式

类加载机制

工作方式:

  • 一个 类加载器 接收到 类加载 请求,会先将这个请求委托给 父类加载器 执行,
  • 父类加载器 还存在 父类加载器,则一直向上委托,直到 Bootstrap ClassLoader
  • 父类加载器 完成加载任务,则直接返回成功结果,否则的话,则交由 子类 尝试加载,如果子类也加载失败,则抛出 ClassNotFoundException

类加载的方式

  • 命令行应用启动 交给 JVM 初始化加载
  • 通过 Class.forName() 来完成动态加载
  • 通过 ClassLoader.loadClass() 动态加载

其他加载类的方式

JVM 体系中,并不是所有的类加载都是 使用的 双亲委派机制 加载的,也有一些是使用的是 SPI(Service Provider Interface) 服务提供者接口实现的。

定义 SPI,是为了允许接口的实现由第三方提供,诸如 JDBCJNDI 等等,这些 SPI 的接口都是属于 Java 核心库,一般在 rt.jar 中,由 Bootstrap ClassLoader 加载, 但是 Bootstrap ClassLoader 是由第三方提供实现的,所以它无法直接加载这些实现类,且同时存在 双亲委派机制 的迷失,Bootstrap ClassLoader 也无法反向委托给其他 ClassLoader 来加载 SPI 的实现类 这样的,就需要一种特殊的类加载器来完成加载这种第三方提供实现的类库(线程上下文类加载器

双向委托和反双向委托

从上图可以看出:

  • 左侧为 双向委托模型

上面介绍了,接收到加载请求,向父类加载器委托,这里不重复了

  • 右侧为 反双向委托模型

原因 由第三方提供了这些核心的SPI接口的实现,而 Bootstrap ClassLoader 并不能加载除了除了rt.jar以外的包且受限双向模式,无法直接加载第三方实现提供的 jar 包. 只能委托 (中间商) 线程上下文类加载器 ContextClassLoader 去加载 jdbc.jar 里面实现的 SPI 核心接口的实现类 到内存中。

其在执行过程中,抛弃了 双亲委派加载链 的模式,使得程序可以逆向使用类加载器,其实是对类加载器的一个补充,使得其更灵活

为什么使用 双亲委派机制

OK,回答这个问题,我们先假设并没有这个限制,在 rt.jar 中随便找一个类 HashMap, 我同时在自定义的程序里面也写一个一样的类放在 ClassPath 下,那么这两个类就需要采用不同的类加载器, 那时候,系统中出现了两个不同的 HashMap,这样会导致程序混乱。

comments powered by Disqus