Java类加载过程

Java类的生命周期

Java类加载过程是Java虚拟机(JVM)将类的字节码文件()加载到内存并进行初始化的过程。

JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件)

类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段:

  • 加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。

image-20230828093926196

Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?

主要分为加载 、连接(验证、准备、解析)、初始化五个步骤。以下依次解读这五个步骤。

类加载过程

加载

在加载阶段,类加载器将类的字节码文件从文件系统、网络等位置读取到内存中。加载并不表示类的实例化,只是将类的信息加载到内存中。需要注意的是,这个阶段不会初始化类的静态变量和执行静态代码块。

加载这一步主要是通过类加载器 完成的。类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载由 双亲委派模型 决定(不过,我们也能打破由双亲委派模型)。

加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。

验证

在验证阶段,虚拟机对加载的字节码进行验证,确保其符合JVM规范,没有安全漏洞和不良代码。

这个过程是为了防止恶意代码的注入,以及确保类文件的正确性。但是如果程序运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

验证阶段主要由四个检验阶段组成:

  1. 文件格式验证(Class 文件格式检查)。基于该类的二进制字节流进行的,主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java 类型信息的要求。

  2. 元数据验证(字节码语义检查)。确保类文件中的信息与其声明一致,以及符号引用是否正确。虚拟机会验证类、父类、接口是否存在,字段和方法的名称和描述符是否正确,符号引用是否指向有效的实体等。

  3. 字节码验证(程序语义检查)。验证类的字节码是否合法、不会造成类型错误或其它异常。虚拟机会检查代码的类型转换、跳转目标是否合法,以及是否存在不合法的操作码序列等。

  4. 符号引用验证(类的正确性检查)。验证符号引用是否能够被正确解析。这个步骤确保被引用的类、字段、方法等在类加载过程中能够正确找到,并且能够被正确访问。

    符号引用验证的主要目的是确保解析阶段能正常执行,如果无法通过符号引用验证,JVM 会抛出异常,比如:

    • java.lang.IllegalAccessError:当类试图访问或修改它没有权限访问的字段,或调用它没有权限访问的方法时,抛出该异常。
    • java.lang.NoSuchFieldError:当类试图访问或修改一个指定的对象字段,而该对象不再包含该字段时,抛出该异常。
    • java.lang.NoSuchMethodError:当类试图访问一个指定的方法,而该方法不存在时,抛出该异常。

准备

在准备阶段,JVM为类的静态变量分配内存空间(这些内存都将在方法区中分配),并设置默认初始值。这里的初始值通常是数据类型的零值,如0、false、null等。

注意:

  1. 这里只是分配内存并设置初始值,并不包括对静态变量的赋值操作。初始化阶段才会赋值。
  2. 这时候进行内存分配的仅包括类变量( Class Variables ,即静态变量,被 static 关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。

解析

解析阶段是将符号引用转换为直接引用的过程。

符号引用是编译时生成的、包含类、接口、字段或方法的全限定名的引用。直接引用是指向内存中实际数据的指针。解析有助于确定符号引用所对应的具体内存位置,以便在运行时能够正确访问。

初始化

初始化阶段是类加载的最后一个阶段,也是最关键的一步。在这个阶段,JVM会执行类的构造器<clinit>()方法的代码,这个方法负责对静态变量进行初始化和执行静态代码块。

注意:

  1. <clinit> ()方法是编译之后自动生成的。
  2. 父类的初始化在子类初始化之前发生
  3. 接口并不会在此阶段初始化。

类卸载

卸载类即该类的 Class 对象被 GC。

卸载类需要满足 3 个要求:

  1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
  2. 该类没有在其他任何地方被引用
  3. 该类的类加载器的实例已被 GC

Java的类卸载通常是由JVM自动管理的,并不是开发者直接控制的。JVM的类加载器通常采用双亲委派模型,其中的类加载器有一个层次结构。当一个类加载器加载了一个类,它会一直在其类加载器的作用域内,直到不再需要。当类不再被需要时,类加载器及其加载的类可能会被卸载。

在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。

类加载器

简介

类加载器ClassLoader的主要作用就是加载 Java 类的字节码( .class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象)

  • 类加载器 ClassLoader 是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
  • 每个 Java 类都有一个引用指向加载它的 ClassLoader
  • 数组类不是通过 ClassLoader 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。
  • 类加载器也可以加载文本、图像等资源,这里只讨论其核心功能 : 加载类 。

Java类加载器的分类

在Java中,类加载器分为以下几种类型

  1. 启动类加载器(Bootstrap Class Loader):最顶层的加载类。是JVM的内置类加载器,负责加载JVM运行时需要的核心类,如java.lang.Objectjava.lang.String等。这个加载器通常由JVM实现提供,不是Java代码实现的。
  2. 扩展类加载器(Extension Class Loader):这个类加载器负责加载JRE扩展目录(jre/lib/ext)下的JAR文件中的类。这些类可以扩展JVM的功能。
  3. 应用程序类加载器(Application Class Loader):它负责加载应用程序的类,即开发者自己编写的类,负责加载当前应用 classpath 下的所有 jar 包和类。
  4. 自定义类加载器(Custom Class Loader):开发者可以通过继承java.lang.ClassLoader类来创建自己的类加载器。自定义类加载器可以用于实现各种加载策略,例如从网络、数据库或其他非传统位置加载类文件。

双亲委派模型

JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。

对于已经加载的类会被放在 ClassLoader 中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。

类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载呢?这就需要提到双亲委派模型了。

双亲委派的具体内容就是:

一个类加载器在加载某一个类的时候,首先委派给他的父类加载器去加载,依次递归,所以所有的加载请求其实都会汇聚到启动类加载器里面,如果父类加载器可以完成这个类的加载,那么就成功返回,如果加载不了,再由子类加载器进行加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public Class<?> loadClass(String name) throws ClassNotFoundException {  
return loadClass(name, false);
}

protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {

// 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
try {
if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else { // 递归终止条件
// 由于启动类加载器无法被Java程序直接引用,因此默认用 null 替代
// parent == null就意味着由启动类加载器尝试加载该类,
// 即通过调用 native方法 findBootstrapClass0(String name)加载
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器不能完成加载请求时,再调用自身的findClass方法进行类加载,若加载成功,findClass方法返回的是defineClass方法的返回值
// 注意,若自身也加载不了,会产生ClassNotFoundException异常并向上抛出
c = findClass(name);
}
}
//是否需要连接该类
if (resolve) {
resolveClass(c);
}
return c;
}

双亲委派的意义:

1、保证了每个类只加载一次,不会被重复加载;(双亲委派模型中的类加载器在加载类时会首先向上委派给父加载器,并且在每一级委派之前都会检查是否已加载,这就确保了类只会被加载一次。如果某个类加载器已经加载了一个类,那么它的父加载器会跳过加载步骤,直接返回已加载的类,从而避免了类的重复加载。)

2、保证了一些核心类库的安全性,一些规定好的只能是启动类加载器加载的那些。(由于核心类库是由启动类加载器加载的,应用程序的类加载器无法修改或篡改这些类。这有助于维护Java的核心类库的一致性和安全性。比如如果自定义了一个Integer类,但是由于双亲委派,最终还是会加载使用bootstrapClassLoader(启动类加载器)加载的那个类。)

打破双亲委派模型

在双亲委派模型中,类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派父加载器完成(调用父类的 loadClass()方法

因此,要打破双亲委派机制,则重写 loadClass()方法即可。

loadClass方法中,可以实现自己的类加载逻辑,而不是按照双亲委派模型的方式委派给父类加载器。这将允许您在应用程序中加载自定义的类,并绕过父类加载器。