虚拟机类加载机制

425 阅读6分钟

参考周志明《深入理解 Java 虚拟机》

参考张龙深入理解 JVM 教程

更好的阅读体验

详细的代码实现

类加载机制

Java虚拟机将类的数据从Class文件加载到内存中进行一系列的操作最终形成可以直接被Java虚拟机直接使用的类型,这个过程就是虚拟机的类加载机制。

在Java代码中,类的加载、连接与初始化过程都是在程序运行期间完成的。Java程序中.class文件才是能够被运行起来的

类加载的过程

JVM类加载的一般过程顺序为:加载、验证、准备、解析、初始化、使用、卸载。其中验证、准备、解析这三个过程统称为连接

  • 加载:查找并加载类的二进制数据,主要是将它们加载到内存当中。

  • 验证:确保被加载的类的正确性, 却包class文件中的字节流中包含的信息符合Java虚拟机规范,这样能够防止字节码篡改而危害虚拟机安全。

  • 准备:为类的静态变量分配内存,并将其初始化为默认值,很明显这个时候还未创建对象。

      public static int value = 123;  // 准备阶段会将value初始int类型的默认值0,而不是123
    
  • 解析:把类中的符号引用转化为直接引用

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

    public static int value = 123; // 这个过程会将value赋予123
    

重点介绍类初始化之前先介绍 Java 程序对类的两种使用方式

  • 主动使用
  • 被动使用

主动使用的方式:

  1. 创建类的实例

  2. 访问某个类或接口的静态变量或者对该静态变量赋值

  3. 调用类的静态方法

  4. 反射: Class.forName("com.test.Test")

  5. 初始化一个类的子类,主动使用该类

  6. JVM启动时被标明为启动类的类

  7. jdk1.7后开始提供的动态语言支持

除了上述的情况之外一般都可认为被动使用,被动引用不会导致类的初始化

类的初始化:

每个类或接口被Java程序 “首次主动使用”才会初始化它们,并且被动使用不会导致类的初始化

  1. 通过子类来引用父类中定义的静态字段,只会触发父类的初始化,而不会触发子类的初始化,即只会初始化直接定义这个字段的类

  2. 常量在编译阶段会存入到调用类(使用类)的常量池中,本质上没有主动引用到定义常量的类,因此不会导致类的初始化,这样将编译后的定义常量所在类的.class文件删除后也不会影响调用类的程序运行, 但是如果这个常量是在非编译期确定的,那么它的值就不会放到调用类的常量池中,这时会导致主动使用这个常量所在的类,进而会导致该类的初始化

  3. 通过数组定义来引用类,不会触发此类的初始化,因为它是由虚拟机动态的自动生成的,直接继承Object的

  4. 在对一个接口进行初始化的时候,不需要对其父接口进行初始化,即一个接口不会因为它的子接口或实现类的初始化而初始化,只有真正使用父接口的时候(如引用接口所定义的常量的时候)才会初始化,另外初始化一个类的时候,并不会先初始化它所实现的接口


类加载器

加载类的工具, 通过一个类的全限定名(binary name) 来获取描述该类的二进制字节流

双亲委托模型

  • 启动类加载器: 主要负责加载加载 Java 的核心类库,主要是/lib路径下的rt.jar
  • 扩展类加载器: 主要负责加载扩展 Java SE 功能的类, 主要在/lib/ext目录下
  • 应用程序类加载器: 复杂加载用户类路径(ClassPath) 上所有的类库,一般是程序的默认加载器

双亲委托模型的工作过程:类加载器收到类加载器的请求,但它首先不会自己尝试加载这个类,而是首先把这个请求交给父加载器取完成,如果父加载器加载不了,自己再尝试加载。

注意:

  1. 双亲委托模型除了启动类加载器,其余加载器都有自己的父加载器。
  1. 双亲委托模型并非强制约束,在一定情况下可以打破。
  1. 类加载器之间的关系是组合,可以理解为集合的关系,父加载器是子加载器的子集,一般是非继承关系。
  1. 子加载器的类能够访问父加载器的类,而父加载器的类无法访问子加载器的类

知道了类加载器的分类,那么如何知道一个类的类加载器ClassLoader

  1. 获得当前类的ClassLoader: class.getClassLoader()

  2. 获得当前线程上下文的ClassLoader: Thread.currentThread().getContextClassLoader()

  3. 获得系统的ClassLoader: ClassLoader.getSystemClassLoader()

思考: 类加载器和加载其他的类,那么类加载器是怎么加载自身的呢?

AppClassLoader, ExtClassLoader都是java编写的类,这些类的加载都是由BootstrapClassLoader(C++编写,内嵌在JVM中,Java启动时BootstrapClassLoader会加载java.lang.classLoader) 加载的, 启动类加载器不是Java类,其他的加载器都是Java类(继承java.lang.ClassLoader) 除此之外,BootstrapClassLoader还会加载JRE正常运行所需的基本组件,即java.util.和java.lang包中的类

面试题:双亲委托模型的好处:

  1. 使类加载器具备一种带有优先级的层次关系,确保Java核心类库的安全,确保Java核心类都是由启动类加载器完成,从而确保核心类不会被用户自定义同名的类的替换

  2. 不同的类加载器可以为相同名称的类创建额外的命名空间,即一个类可以由用户自定义的不同的类加载器加载多次,那么这些创建的类也是不相同的,而且这些类也是不兼容的,相当于创建了相互隔离的类空间


打破双亲委托模型

SPI,服务提供者接口,这些接口属于Java核心库,由启动类加载器加载,例如JDBC, Java 提供接口规范,由数据库厂商实现这些接口。而数据库厂商提供的jar包一般都是在ClassPath路径中,而在这个路径中启动类加载器是不能加载的,因此就出现了一种特殊的类加载器:线程上下文加载器

线程上下文类加载器:

ContextClassLoader: Thread类中 getContextClassLoadersetContextClassLoader(ClassLoader cl)分别用来获得和设置上下文加载器,如果没有通过setContextClassLoader(ClassLoader cl)进行设置的话,那么会继承父线程的加载器,而Java应用的初始线程类加载器是应用程序类加载器,所以一般这个类加载器就默认是应用程序类加载器

线程上下文类加载器使用的伪代码:

线程上下文类加载器的一般使用模式(广泛用于框架之中: 获取 - 使用 - 还原)
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();// 获取
try {
    Thread.currentThread().setContextClassLoader(targetLoader);  
    myMethod();// 使用
} finally {
    Thread.currentThread().setContextClassLoader(classLoader);// 最后一定要还原
}

线程类上下文类加载器广泛用于框架开发中。

本文为学习笔记,如有错误,欢迎指正