前言

视频教程:https://www.bilibili.com/video/BV1Dz4y1A7FB?p=2&vd_source=85ac5ee1b07df12a44b648a8751d30f6

相关知识点

类加载都加载哪些

1
在Java中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载。

Java类加载过程/加载Class文件的原理机制

image-20240405145730071

加载/装载 --> 链接 --> 初始化 --> 使用 --> 卸载

image-20240405150832032

装载阶段

装载阶段都干什么

1
2
3
4
简而言之就是将Java类的二进制数据流加载到机器内存中,并在内存中构建出Java类的原型——类模板对象。
- 通过类的全限定名,获取类的二进制数据流。
- 解析类的二进制数据流为方法区内的数据结构(Java类模型)
- 创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口

类模板对象

1
类模板对象:类在JVM内存中的快照。JVM解析类字节码文件,将常量池、类字段、类方法等信息存储到类模板中。在JVM运行期从类模板中获取任意类的信息。(反射机制基于这一信息)

类模板对象的位置

1
2
3
存储在方法区
- JDK1.8之前:永久代
- 1.8之后:元空间

二进制流的获取方式

1
2
3
4
5
6
7
只要所读取的二进制流(字节码)符合JVM规范即可
- 虚拟机可能通过文件系统读入一个class后缀的文件(最常见)
- 读入jar、zip等归档数据包,提取类文件。
- 事先存放在数据库中的类的二进制数据
- 使用类似于HTTP之类的协议通过网络进行加载
- 在运行时生成一段Class的二进制信息等
在获取到类的二进制信息后,Java虚拟机就会处理这些数据,并最终转为一个java.lang.Class的实例。

Class实例(对象)的位置

1
2
JVM解析类模板(方法区)以后,会在堆中创建一个Class对象。外部可以通过访问Class对象获取该类模板的数据结构。
Class实例的构造方法是私有的,只有JVM能够创建。

数组类的加载

1
2
3
4
数组类本身并不是由类加载器负责创建,而是由JVM在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建。
1. 如果数组的元素类型是引用类型,那么就遵循定义的加载过程递归加载和创建数组A的元素类型;
2. JVM使用指定的元素类型和数组维度来创建新的数组类。
3. 如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定。否则数组类的可访问性将被缺省定义为public。

链接阶段

链接过程之验证阶段(Verification)

目的是保证加载的字节码是合法、合理并符合规范的。

  • 其中格式验证会和装载阶段一起执行。验证通过之后,类加载器才会成功将类的二进制数据信息加载到方法区中。
  • 格式验证之外的验证操作将会在方法区中进行。
  • 符号引用的验证在链接阶段的解析时进行。

image-20240405155919139

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
格式验证:
是否以魔数 OxCAFEBABE开头,主版本和副版本号是否在当前Java虚拟机的支持范围内,数据中每一个项是否都拥有正确的长度等。

语义检查:
- 是否所有的类都有父类的存在(在Java里,除了Object外,其他类都应该有父类)
- 是否一些被定义为final的方法或者类被重写或继承了
- 非抽象类是否实现了所有抽象方法或者接口方法
- 是否存在不兼容的方法(比如方法的签名除了返回值不同,其他都一样,这种方法会让虚拟机无从下手调度;abstract情况下的方法,就不能是final的了)

字节码验证(最复杂的一个过程):
Java虚拟机还会进行字节码验证。它试图通过对字节码流的分析,判断字节码是否可以被正确地执行。
- 在字节码的执行过程中,是否会跳转到一条不存在的指令
- 函数的调用是否传递了正确类型的参数
- 变量的赋值是不是给了正确的数据类型等
栈映射帧(StackMapTable)就是在这个阶段,用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型。如果在这个阶段无法通过检查,虚拟机也不会正确装载这个类。但是,如果通过了这个阶段的检查,也不能说明这个类是完全没有问题的。
栈映射帧的概念,就是表示在执行某一条字节码指令之前,帧的状态,即局部变量表和操作数栈的状态,不是每条字节码前面都有栈映射帧,通常在有条件跳转或无条件跳转之后或者抛出异常之前。

符号引用的验证:
Class文件在其常量池会通过字符串记录自己将要使用的其他类或者方法。因此,在验证阶段,虚拟机就会检查这些类或者方法确实是存在的,并且当前类有权限访问这些数据,如果一个需要使用类无法在系统中找到,则会抛出NoClassDefFoundError,如果一个方法无法被找到,则会抛出NoSuchMethodError。
此阶段在解析环节才会执行。

链接过程之准备阶段(Preparation)

  • static 修饰的静态变量(没有final修饰)分配内存,并将其==初始化为默认值==。默认是都是0,或者\u0000 或者null(引用类型)。
  • static final修饰的静态常量则会==显式赋值==
1
2
3
4
5
6
7
- Java并不支持boolean类型,对于boolean类型,内部实现是int,由于int的默认值是0,故对应的,boolean的默认值就是false

- 这里不包含基本数据类型的字段用static final修饰的情况,因为final在编译的时候就会分配了,准备阶段会显式赋值。

- 注意这里不会为实例变量分配初始化,实例变量是会随着对象一起分配到Java堆中。

- 在这个阶段并不会像初始化阶段中那样会有初始化或者代码被执行。

链接过程之解析阶段(Resolution)

将类、接口、字段和方法的符号引用转为直接引用。

1
2
- 通过解析操作,符号引用就可以转变为目标方法在类中方法表中的位置,从而使得方法被成功调用。
- 如果直接引用存在,那么可以肯定系统中存在该类、方法或者字段。但只存在符号引用,不能确定系统中一定存在该结构。

初始化阶段

为类的静态变量赋予正确的初始值。(显式初始化)

类的初始化是类装载的最后一个阶段。如果前面的步骤都没有问题,那么表示类可以顺利装载到系统中。此时,类才会开始执行Java字节码。(即:到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。)

==初始化阶段的重要工作是执行类的初始化方法:<clinit>()方法。==

  • 该方法仅能由Java编译器生成并由JVM调用,程序开发者无法自定义一个同名的方法,更无法直接在Java程序中调用该方法,虽然该方法也是由字节码指令所组成。
  • 它是由类静态成员的赋值语句以及static语句块合并产生的。

<clinit>() : 只有在类中的==static的变量显式赋值==或在==静态代码块中赋值了==。才会生成此方法。
<init>() 一定会出现在Class的method表中。

子类加载前先加载父类?

1
在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的<clinit>总是在子类<clinit>之前被调用。也就是说,父类的static块优先级高于子类。 

哪些类不会生成<clinit>方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 一个类中并没有声明任何的类变量,也没有静态代码块时
- 一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时
- 一个类中包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式

/**
* @author shkstart
* 哪些场景下,java编译器就不会生成<clinit>()方法
*/
public class InitializationTest1 {
//场景1:对于非静态的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
public int num = 1;
//场景2:静态的字段,没有显式的赋值,不会生成<clinit>()方法
public static int num1;
//场景3:比如对于声明为static final的基本数据类型的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法。【已在准备阶段赋值,编译时初始化】
public static final int num2 = 1;
}

static和final的搭配问题

1
2
- 普通基本数据类型和引用类型(即使是常量)的静态变量,是需要额外调用putstatic等JVM指令的,这些是在显式初始化阶段执行,而不是准备阶段调用
- 基本数据类型常量(非调用方法的显式赋值)、String类型字面量的定义方式的常量,则不需要这样的步骤,是在准备阶段完成的。

<clinit>()的调用会死锁吗

1
2
3
4
5
因为函数<clinit>()带锁线程安全的,因此,如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁。

排查方式:
1. 在控制台用jps指令获取对应线程号
2. 使用jstack 线程号 指令看一下字节码的执行情况

类的主动使用

Class只有在必须要首次使用的时候才会被装载,Java虚拟机不会无条件地装载Class类型。Java虚拟机规定,一个类或接口在初次使用前,必须要进行初始化。这里指的“使用”,是指主动使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
主动使用只有下列几种情况:
1. 当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化。
2. 当调用类的静态方法时,即当使用了字节码invokestatic指令。
3. 当使用类、接口的静态字段时(final修饰特殊考虑),比如,使用getstatic或者putstatic指令。
4. 当使用java.lang.reflect包中的方法反射类的方法时。比如:Class.forName("com.atguigu.java.Test")
5. 当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
6. 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化。
7. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
8. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。(涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄对应的类)

针对5,补充说明:
当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。
- 在初始化一个类时,并不会先初始化它所实现的接口
- 在初始化一个接口时,并不会先初始化它的父接口
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态字段时,才会导致该接口的初始化。

针对7,说明:
JVM启动的时候通过引导类加载器加载一个初始类。这个类在调用public static void main(String[])方法之前被链接和初始化。这个方法的执行将依次导致所需的类的加载,链接和初始化。

类的被动使用

除了以上的情况属于主动使用,其他的情况均属于被动使用。被动使用不会引起类的初始化。
也就是说:并不是在代码中出现的类,就一定会被加载或者初始化。如果不符合主动使用的条件,类就不会初始化。

1
2
3
4
5
1. 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。
比如:当通过子类引用父类的静态变量,不会导致子类初始化
2. 通过数组定义类引用,不会触发此类的初始化
3. 引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了。
4. 调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

被动的使用,意味着不需要执行初始化环节,意味着没有<clinit>()的调用。

如何追踪类的加载信息

1
设置参数-XX:+TraceClassLoading

使用阶段

开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法),或者使用new关键字为其创建对象实例。

类的卸载

类的Class、类的加载器、类的实例之间的引用关系

类的Class和类的加载器之间为==双向关联关系==。

  • 类加载器的内部实现中,用一个Java集合来存放该加载器所加载类的引用
  • 一个类的Class对象总是会引用它的类加载器,调用Class对象的getClassLoader()方法,就能获得它的类加载器

类的实例引用这个类的Class对象,==单向关系==

  • 在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用
  • 所有的Java类都有一个静态属性class,它引用代表这个类的Class对象。

image-20240405170433390

类何时被卸载

1
2
3
4
5
6
7
8
以上图为例,
当代表Sample类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,Sample类在方法区内的数据也会被卸载,从而结束Sample类的生命周期。

loader1变量和obj变量间接引用代表Sample类的Class对象,而objClass变量则直接引用它。

如果程序运行过程中,将上图左侧三个引用变量都置为null,此时Sample对象结束生命周期,MyClassLoader对象结束生命周期,代表Sample类的Class对象也结束生命周期,Sample类在方法区内的二进制数据被卸载。

当再次有需要时,会检查Sample类的Class对象是否存在,如果存在会直接使用,不再重新加载;如果不存在Sample类会被重新加载,在Java虚拟机的堆区会生成一个新的代表Sample类的Class实例(可以通过哈希码查看是否是同一个实例)。

类卸载在实际生产中的情况如何?

1
2
3
4
5
6
7
(1) 启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm和jls规范)

(2) 被系统类加载器和扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到,其达到unreachable的可能性极小。

(3) 被开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到。可以预想,稍微复杂点的应用场景中(比如:很多时候用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的(至少卸载的时间是不确定的)。

综合以上三点,一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的。同时我们可以看的出来,开发者在开发代码时候,不应该对虚拟机的类型卸载做任何假设的前提下,来实现系统中的特定功能。

方法区的垃圾回收

主要回收两部分内容:常量池中废弃的常量和不再使用的类型。

1
2
3
4
5
废弃的常量:常量池中的常量没有被任何地方引用,就可以被回收。
不再使用的类型:需要同时满足下面三个条件:
- 该类所有的实例都已经被回收。也就是Java堆中不存在该类及其任何派生子类的实例。
- 加载该类的类加载器已经被回收。这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

类加载器

类加载器的作用/ClassLoader的作用

JVM 执行类加载机制的前提。==ClassLoader在整个装载阶段,只能影响到类的加载==

1
ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等操作。

image-20240405171820082

类的加载分类:显式加载 vs 隐式加载

1
2
3
class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式。
- 显式加载:指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象。
- 隐式加载:则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。

加载的类是否唯一

判断加载类的唯一性:同一个类(字节码文件)、同一个类加载器

类加载机制的基本特征

1
2
3
4
5
- 双亲委派模型。但不是所有类加载都遵守这个模型,有的时候,启动类加载器所加载的类型,是可能要加载用户代码的,比如JDK内部的ServiceProvider/ServiceLoader机制,用户可以在标准API框架上,提供自己的实现,JDK也需要提供些默认的参考实现。例如,Java 中JNDI、JDBC、文件系统、Cipher等很多方面,都是利用的这种机制,这种情况就不会用双亲委派模型去加载,+而是利用所谓的上下文加载器。

- 可见性。子类加载器可以访问父加载器加载的类型,但是反过来是不允许的。不然,因为缺少必要的隔离,我们就没有办法利用类加载器去实现容器的逻辑。

- 单一性。由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载。但是注意,类加载器“邻居”间,同一类型仍然可以被加载多次,因为互相并不可见。

类加载器的种类

JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。

引导类加载器:即Bootstrap ClassLoader,C和C++语言编写。

自定义类加载器:其他类加载器,Java语言编写。

image-20240405172841285

引导类加载器

1
2
3
4
5
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部。
- 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)。用于提供JVM自身需要的类。
- 并不继承自java.lang.ClassLoader,没有父加载器。
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。

扩展类加载器

1
2
3
4
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
继承于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

系统类加载器

1
2
3
4
5
6
7
- java语言编写,由sun.misc.Launcher$AppClassLoader实现
- 继承于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性 java.class.path 指定路径下的类库
- 应用程序中的类加载器默认是系统类加载器。
- 它是用户自定义类加载器的默认父加载器
- 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器

获取类加载器的途径

1
2
3
4
5
6
7
8
获得当前类的ClassLoader
- clazz.getClassLoader()

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

获得系统的ClassLoader
- ClassLoader.getSystemClassLoader()

深入分析ClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
主要方法:

- public final ClassLoader getParent()
返回该类加载器的超类加载器

- public Class<?> loadClass(String name) throws ClassNotFoundException
加载名称为name的类,返回结果为java.lang.Class类的实例。如果找不到类,则返回 ClassNotFoundException 异常。该方法中的逻辑就是双亲委派模式的实现。

- protected Class<?> findClass(String name) throws ClassNotFoundException
查找二进制名称为name的类,返回结果为java.lang.Class类的实例。这是一个受保护的方法,JVM鼓励我们重写此方法,需要自定义加载器遵循双亲委托机制,该方法会在检查完父类加载器之后被loadClass()方法调用。

-protected final Class<?> defineClass(String name, byte[] b, int off, int len)
根据给定的字节数组b转换为Class的实例,off和len参数表示实际Class信息在byte数组中的位置和长度,其中byte数组b是ClassLoader从外部获取的。这是受保护的方法,只有在自定义ClassLoader子类中可以使用。
defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象

- protected final void resolveClass(Class<?> c)
链接指定的一个Java类。使用该方法可以使用类的Class对象创建完成的同时也被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。

- rotected final Class<?> findLoadedClass(String name)
查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例。这个方法是final方法,无法被修改。

- private final ClassLoader parent;
它也是一个ClassLoader的实例,这个字段所表示的ClassLoader也称为这个ClassLoader的双亲。在类加载的过程中,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
35
36
37
38
39
涉及到对如下方法的调用:
protected Class<?> loadClass(String name, boolean resolve) //resolve:true-加载class的同时进行解析操作。
throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) { //同步操作,保证只能加载一次。
//首先,在缓存中判断是否已经加载同名的类。
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//获取当前类加载器的父类加载器。
if (parent != null) {
//如果存在父类加载器,则调用父类加载器进行类的加载
c = parent.loadClass(name, false);
} else { //parent为null:父类加载器是引导类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) { //当前类的加载器的父类加载器未加载此类 or 当前类的加载器未加载此类
// 调用当前ClassLoader的findClass()
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {//是否进行解析操作
resolveClass(c);
}
return c;
}
}

SecureClassLoader 与 URLClassLoader

1
2
3
4
5
SecureClassLoader:
SecureClassLoader扩展了 ClassLoader,新增了几个与使用相关的代码源(对代码源的位置及其证书的验证)和权限定义类验证(主要指对class源码的访问权限)的方法,一般我们不会直接跟这个类打交道,更多是与它的子类URLClassLoader有所关联。

URLClassLoader:
ClassLoader是一个抽象类,很多方法是空的没有实现,比如 findClass()、findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现。并新增了URLClassPath类协助取得Class字节码流等功能。

在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

ExtClassLoader 与 AppClassLoader

1
2
- ExtClassLoader并没有重写loadClass()方法,这足矣说明其遵循双亲委派模式
- AppClassLoader重载了loadClass()方法,但最终调用的还是父类loadClass()方法,因此依然遵守双亲委派模式。

Class.forName() 与 ClassLoader.loadClass()

1
2
3
- Class.forName():是一个静态方法,最常用的是Class.forName(String className);根据传入的类的全限定名返回一个 Class 对象。该方法在将 Class 文件加载到内存的同时,会执行类的初始化。如: Class.forName("com.atguigu.java.HelloWorld");

- ClassLoader.loadClass():这是一个实例方法,需要一个 ClassLoader 对象来调用该方法。该方法将 Class 文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。该方法因为需要得到一个 ClassLoader 对象,所以可以根据需要指定使用哪个类加载器。

为什么要自定义类加载器

1
2
3
4
5
6
7
8
- 隔离加载类
在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如:阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。再比如:Tomcat这类Web应用服务器,内部自定义了好几种类加载器,用于隔离同一个Web应用服务器上的不同应用程序。 (类的仲裁-->类冲突)
- 修改类加载的方式
类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载
- 扩展加载源
比如从数据库、网络、甚至是电视机机顶盒进行加载
- 防止源码泄漏
Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码。

双亲委派机制

什么是双亲委派机制/双亲委派机制的本质?

1
2
3
4
5
定义:
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载。

本质:
规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载。

双亲委派机制的优缺点

1
2
3
4
5
6
7
8
优点:
- 避免类的重复加载,确保一个类的全局唯一性
Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
- 保护程序安全,防止核心API被随意篡改

缺点:
- 顶层的ClassLoader无法访问底层的ClassLoader所加载的类。(后来为了解决这个问题,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader))
通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问题。比如在系统类中提供了一个接口,该接口需要在应用类中得以实现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这时,就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。

tomcat的类加载机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Tomcat8 和 Tomcat6比较大的区别是 :
Tomcat8可以通过配置 <Loader delegate="true"/>表示遵循双亲委派机制。

当tomcat启动时,会创建几种类加载器:
1. Bootstrap 引导类加载器
加载JVM启动所需的类,以及标准扩展类(位于jre/lib/ext下)
2. System 系统类加载器
加载tomcat启动的类,比如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。位于CATALINA_HOME/bin下。
3. CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader
这些是Tomcat自己定义的类加载器,它们分别加载/common/*、/server/*、/shared/*(在tomcat 6之后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/*中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。
- commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;(加载CATALINA_HOME/lib下的结构,比如servlet-api.jar)
- catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
- sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
- WebappClassLoader:单个Tomcat实例中各个Web应用程序私有的类加载器,加载路径中的class只对当前Webapp可见;(加载WEB-INF/lib和WEB-INF/classes下的结构)

image-20240405175956685

tomcat 违背了java 推荐的双亲委派模型了吗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
违背了。双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当由自己的父类加载器加载。
但tomcat 不是这样实现,tomcat 为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。

Web应用默认的类加载顺序是(打破了双亲委派规则):
①先从JVM的BootStrapClassLoader中加载。
②加载Web应用下/WEB-INF/classes中的类。
③加载Web应用下/WEB-INF/lib/*.jap中的jar包中的类。
④加载上面定义的System路径下面的类。
⑤加载上面定义的Common路径下面的类。

如果在配置文件中配置了` <Loader delegate="true"/>`,那么就是遵循双亲委派规则,加载顺序如下:
①先从JVM的BootStrapClassLoader中加载。
②加载上面定义的System(应用类加载器)路径下面的类。
③加载上面定义的Common路径下面的类。
④加载Web应用下/WEB-INF/classes中的类。
⑤加载Web应用下/WEB-INF/lib/*.jap中的jar包中的类。

JDK9中类加载结构的变化

image-20240405180454786

1
2
3
4
5
6
7
8
9
10
11
1. 平台类加载器和应用程序类加载器都不再继承自 java.net.URLClassLoader。
现在启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader。

2. Java 9中,类加载器有了名称。该名称在构造方法中指定,可以通过getName()方法来获取。平台类加载器的名称是platform,应用类加载器的名称是app。类加载器的名称在调试与类加载器相关的问题时会非常有用。

3. 启动类加载器现在是在jvm内部和java类库共同协作实现的类加载器(以前是 C++实现),但为了与之前代码兼容,在获取启动类加载器的场景中仍然会返回null,而不会得到BootClassLoader实例。

4. 类加载的委派关系也发生了变动。
当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。

5. 扩展机制被移除,扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器(platform class loader)。可以通过ClassLoader的新方法getPlatformClassLoader()来获取。

image-20240405180730166

面试题

类加载的时机(百度)/哪些情况会触发类的加载(京东)

1
即类的主动使用

Class的forName(“Java.lang.String”)和Class的getClassLoader()的loadClass(“Java.lang.String”)有什么区别? (百度)

1
即主动使用、被动使用

JVM类加载机制 (滴滴)

1
即类的加载过程

什么是类加载器,类加载器有哪些?(字节跳动)

1
即类加载器的种类

深入分析ClassLoader(蚂蚁金服)

1
即深入分析ClassLoader

手写一个类加载器Demo (百度)

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//继承ClassLoader类,重写findclass方法。
public class MyClassloader extends ClassLoader {

private String path;
private String classloaderName;

public MyClassloader(String path,String classloaderName){
this.path = path;
this.classloaderName = classloaderName;
}

//用于寻找类文件
@Override
public Class findClass(String name){
byte[] b =loadClassData(name);
return defineClass(name,b,0,b.length);
}

public byte[] loadClassData(String name) {
name = path + name + ".class";
InputStream in = null;
ByteArrayOutputStream out = null;

try {
in = new FileInputStream(new File(name));
out = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) != -1){
out.write(i);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
out.close();
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return out.toByteArray();
}
}


// 创建测试类,测试结果
public class ClassLoderCheck {

public static void main(String[] args)
throws IllegalAccessException, InstantiationException, ClassNotFoundException {
MyClassloader classloader = new MyClassloader("D:/jvm/", "myclasscloderz");
Class c = classloader.loadClass("World");
System.out.println(c.getClassLoader());
System.out.println(c.getClassLoader().getParent());
System.out.println(c.getClassLoader().getParent().getParent());
c.newInstance();
}
}


双亲委派机制及使用原因 (蚂蚁金服)

1
同什么是双亲委派机制/双亲委派机制的本质?

双亲委派机制可以打破吗?为什么 (京东)

1
同双亲委派机制的优缺点中的缺点

请解释tomcat的类加载机制?(阿里)

1
同tomcat的类加载机制