文章目录
- 前言
- 一、类加载过程
- 1.1 加载(Loading)
- 1.2 验证(Verification)
- 1.3 准备(Preparation)
- 1.4 解析(Resolution)
- 1.5 初始化(Initialization)
- 二、双亲委派模型
- 2.1 类加载器
- 2.2 什么是双亲委派模型
- 2.3 双亲委派模型的解决的问题
- 2.4 破坏双亲委派模型
前言
在 Java 中,类加载机制是 Java 虚拟机(JVM)的一个重要组成部分,它负责在运行时将 Java 类加载到内存中,并转换为可执行代码。理解类加载机制对于深入理解 Java 的运行机制和开发高质量的Java应用程序至关重要。
本文将深入探讨 Java 的类加载过程以及双亲委派模型。首先,我将详细介绍类加载过程的五个阶段。接下来,将重点介绍双亲委派模型以及它解决的问题。
一、类加载过程
1.1 加载(Loading)
加载是类加载的第一个阶段。在这个阶段,JVM 会根据类的全限定名(Fully Qualified Name)找到对应的字节码文件,并将其加载到内存中。加载阶段不仅仅包括从文件系统中读取字节码,还可能包括从网络、数据库等地方加载类的字节码。
详细说来,类的加载阶段需要完成三个任务:
-
获取字节流:
Java虚拟机根据类的全限定名(Fully Qualified Name)来获取定义该类的二进制字节流。这个过程可以通过从本地文件系统、网络、JAR包等位置读取类的字节码文件,并将其存储在内存中。 -
转化为运行时数据结构:
在获取类的字节流后,Java虚拟机将这个字节流所代表的静态存储结构(如类、字段、方法、常量池等)转化为方法区的运行时数据结构。这个过程包括解析字节码中的各种信息,并生成对应的运行时数据结构,用于在运行时执行类的各种操作。 -
生成java.lang.Class对象:
在内存中生成一个代表这个类的java.lang.Class
对象。这个Class
对象是在JVM中表示类的元数据信息的对象,通过这个对象可以访问类的方法、字段、构造函数等信息,以及执行类的各种操作。这个Class
对象也是Java程序中获取类的入口,通过它可以访问类的各种静态和动态信息。
注意:
“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,它和类加载 (Class Loading) 是不同的,一个是加载 (Loading)另一个是类加载 (Class Loading),不要把二者混淆。
1.2 验证(Verification)
验证是类加载过程的第二个阶段。在验证阶段,JVM 会对加载的字节码进行验证,确保字节码的结构是合法的、符合规范的,不包含安全漏洞和不符合 JVM 规范的内容。这个阶段是确保类加载过程的安全性和正确性的重要步骤。
下图是 Java 虚拟机规范中的 Class 文件的结构定义,同时也是验证阶段所需要验证的:
1.3 准备(Preparation)
准备是类加载过程的第三个阶段。在准备阶段,JVM会为类的静态变量分配内存,并设置初始值(通常是零值)。这里并不包括对静态变量赋值的操作,赋值的操作将在初始化阶段进行。
例如有这样一段代码:
public static int value = 100;
在准备阶段,JVM 会给静态变量 value
分配内存,但是设置的初值是 0
,而不是 100
,因为赋值操作是在初始化阶段完成的。
1.4 解析(Resolution)
解析是类加载过程的第四个阶段。在解析阶段,JVM 会将符号引用替换为直接引用。
- 符号引用是一种在编译期产生的,用于描述类、字段、方法等的引用。
- 直接引用是指直接指向内存中的地址的引用。
解析阶段的目的是将符号引用解析成直接引用,以便在之后的程序执行中更快速地定位到所引用的目标。
1.5 初始化(Initialization)
初始化是类加载过程的最后一个阶段。在初始化阶段,JVM会执行类的初始化代码,包括对静态变量赋值
和执行静态初始化块
。类的初始化是在首次使用类的时候触发的,只有在初始化完成后,类才算是被真正加载和准备好了。
二、双亲委派模型
2.1 类加载器
一提到 JVM 的类加载机制,不由自主的就会想到 双亲委派模型
,而要理解这个模型,首先就需要了解类加载器。
在 JVM 中,默认有三种类加载器:
-
启动类加载器(Bootstrap Class Loader)
- 这是JVM内部实现的特殊类加载器,由 C++ 编写,而不是 Java 类;
- 它负责加载 JVM 自身需要的类,包括核心类库(如
java.lang
包中的类)等; - 启动类加载器是类加载器层次结构的最顶层,没有父加载器;
- 由于其是C++编写的,因此在Java代码中无法直接引用它。
-
扩展类加载器(Extension Class Loader)
- 扩展类加载器是 Java 类,由
sun.misc.Launcher$ExtClassLoader
实现; - 它负责加载 JRE(Java Runtime Environment)的扩展目录
lib/ext
中的类; 扩展类加载器
是启动类加载器
的子加载器
;- 同时也是类加载器层次结构中的中间层。
应用程序类加载器(Application Class Loader)
- 应用程序类加载器也是 Java 类,由
sun.misc.Launcher$AppClassLoader
实现; - 它负责加载应用程序
classpath
下的类,即我们自己编写的 Java 类
; 应用程序类加载器
是扩展类加载器
的子加载器
;- 同时处于类加载器层次结构中的最底层。
以上三种类加载器构造了 JVM 的类加载器层次结构,即双亲委派模型
。除了启动类加载器
是由 C++ 语言实现外,其他的所有加载器均由 Java 实现,并且都继承了java.lang.ClassLoader
。
下图展示了 JVM 的类加载器的层次结构:
当然,除了默认的这三个类加载器外,开发人员还可以根据自己的需求实现自定义的类加载器。自定义类加载器需要继承自java.lang.ClassLoader
,通过重写findClass
方法来实现特定的类加载逻辑。自定义的类加载器常用于实现插件机制,热部署等功能。
2.2 什么是双亲委派模型
双亲委派模型是 Java 类加载器的一种类加载机制,简单来说,核心思想就是:一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
双亲委派模型的类加载过程大致如下图所示:
- 当一个类加载器收到加载请求的时候(子加载器),它首先会把这个加载请求委派给其父加载器,父加载器可能会继续委托给其父加载器,依次递归。
- 到达类加载器层次结构的最顶层(启动类加载器)后,再尝试进行类加载。
- 如果当前加载器无法加载时,就会将加载任务交给其子加载器,由子加载器尝试进行类加载。
2.3 双亲委派模型的解决的问题
双亲委派模型主要解决了两个问题:
- 类的隔离和防止冲突
- 在复杂的 Java 应用程序中,可能会涉及许多不同的类库和模块。这些类库和模块可能引用了相同的类名,如果不加以限制,可能就会导致类名冲突。
- 双亲委派模型通过层级结构的类加载器,确保每个类加载器都将加载任务优先委派给父加载器进行加载,这样同名的类只会被加载一次,并且加载过程是有序的,避免了类的冲突和混淆。
举个例子:
- 假设应用程序的类加载器需要加载
java.lang.String
类。- 它首先会委派给扩展类加载器,扩展类加载器也找不到,再委派给启动类加载器。因为启动类加载器能够找到并加载
java.lang.String
类,所以它会将该类返回给应用程序类加载器。- 由于类加载器的委派顺序,即便应用程序中有
自定义
的java.lang.String
类,也不会被加载,从而保证了类的隔离和防止冲突。
- 安全性和可信任代码的执行
Java中的核心类库位于JVM内部,它们提供了Java编程语言的基本功能。这些核心类库在 JVM 启动时由启动类加载器加载。通过双亲委派模型,可以确保核心类库只会被启动类加载器加载,而不会被应用程序类加载器或其他自定义类加载器加载
。
这种安排提高了Java程序的安全性,因为核心类库的来源可信,不会被恶意类替代
。如果允许应用程序类加载器直接加载核心类库,那么恶意类可能会替代核心类库中的某些类,从而导致安全漏洞。双亲委派模型通过限制核心类库的加载,确保了可信任代码的执行,并防止恶意类的篡改。
2.4 破坏双亲委派模型
尽管双亲委派模型在大多数情况下是有益的,但是有些特定的场景下就需要破坏它,其中 JDBC
就是一个典型的例子:
-
在 JDBC 中,数据库厂商提供的自己的 JDBC 驱动,这些驱动
实现了 JDBC 标准接口的类库
。由于 JDBC 驱动需要和特定的数据库交互,因此它们通常由数据库厂商提供,而不是 Java 标准的一部分。而数据库厂商非常多,因此 JDBC 驱动的种类也就非常多了。 -
考虑到这种情况,JDBC 驱动就需要被应用程序自己加载,而不是委托给父类。如果此时还是按照
双亲委派模型
的规则进行类加载,那么加载的就是 Java 提供的JDBC 标准接口的类库
中的类了,而不是特定数据库的类。 -
为了解决这个问题,JDBC 驱动的加载通常是通过反射来实现的,应用程序类加载器可以直接加载驱动的类,而不通过双亲委派模型。这样应用程序可以加载自己所需的特定驱动类,而不受父类加载器的限制。