Java 虚拟机入门教程之六 JVM 类加载机制
大纲
- Java 虚拟机入门教程之一 JVM 内存结构
- Java 虚拟机入门教程之二 JVM 垃圾收集
- Java 虚拟机入门教程之三 JVM 参数调优
- Java 虚拟机入门教程之四 JVM 四种引用
- Java 虚拟机入门教程之五 JVM 性能优化
- Java 虚拟机入门教程之六 JVM 类加载机制
双亲委派机制
双亲委派机制(Parent Delegation Model)是 Java 类加载器的一种工作机制,其基本原则是:每个类加载器在接收到类加载请求时,会先将该请求委派给它的父类加载器处理,只有在父类加载器无法完成该请求时,才由自己来加载。通过这种机制,可以防止内存中出现多份同样的字节码(从安全性角度考虑),同时确保了核心类库始终由启动类加载器加载,避免了自定义类库对核心类库的破坏。这种层次化的加载过程有助于提升系统的安全性和稳定性,同时也便于管理不同级别的类库。比如,加载位于 $JAVA_HOME
中 jre/lib/rt.jar
包中的 Object
类时,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object
对象。
- (1) 当一个类加载器(比如应用程序类加载器)接收到类加载请求时,首先会将请求委派给它的父类加载器(扩展类加载器)。
- (2) 扩展类加载器在接收到加载请求后,会继续将请求向上委派给它的父类加载器(启动类加载器)。
- (3) 启动类加载器尝试加载该类,如果加载成功,则加载过程结束;如果加载失败(例如类不在它的搜索路径中),则将加载请求返回给扩展类加载器。
- (4) 扩展类加载器尝试加载该类,如果加载成功,则加载过程结束;如果加载失败,则将加载请求返回给应用程序类加载器。
- (5) 最后,应用程序加载器会尝试自己加载类,如果加载失败,则会抛出
ClassNotFoundException
异常。
Java 类加载的过程
Java 类加载过程是将类的字节码加载到内存中,并将其转换为 Java 类对象的过程。这个过程主要包括以下几个步骤:
1. 加载(Loading):
- 在这个阶段,类加载器从文件系统或网络中读取类的字节码(通常是从
.class
文件中读取)到 JVM 内存中,并创建一个Class
对象来表示这个类。 - 字节码可以从多种不同的来源加载,比如本地文件系统、JAR 包或者网络。
- 在这个阶段,类加载器从文件系统或网络中读取类的字节码(通常是从
2. 连接(Linking):连接阶段分为以下三个子阶段
- 验证(Verification):确保被加载的类的字节码符合 Java 语言规范,并且不会危害 JVM 的安全。这个阶段会进行各种字节码级别的检查,以确保类的结构和行为是合法的。
- 准备(Preparation):为类的静态变量分配内存,并将这些变量初始化为默认值(如 0 或者 Null 等)。这个阶段仅分配内存,尚未执行任何代码。
- 解析(Resolution):将类的符号引用(如类名、方法名等)转换为直接引用(内存地址)。这一步骤可以在连接阶段的任何时候执行,也可以推迟到类被使用的时候执行。
3. 初始化(Initialization):
- 初始化阶段是执行类的静态代码块(
static {}
)和为静态变量赋予正确的初始值(即编写代码时定义的值)。 - 这一步骤是类加载过程的最后一步,只有在这一步完成后,类才会被认为是完全加载的。
- 初始化阶段是执行类的静态代码块(
在类加载的整个过程中,Java 虚拟机会使用双亲委派机制来保证类加载的安全性和稳定性。双亲委派机制要求每个类加载器在接收到类加载请求时,会先将该请求委派给它的父类加载器处理,只有在父类加载器无法完成该请求时,才由自己来加载。
类加载过程的总结
- 加载:读取 Class 文件并创建
Class
对象。 - 连接:验证、准备和解析类的符号引用。
- 初始化:执行静态代码块和静态变量的初始化。
JVM 加载 Class 文件的原理
类加载器负责加载 Class 文件,而 Class 文件在文件开头有特定的文件标识(魔数)。ClassLoader 只负责 Class 文件的加载,至于它是否可以运行,则由 Execution Engine(执行引擎)决定。
魔数的概念
- Class 文件开头的四个字节的无符号整数称为魔数(Magic Number)。
- 魔数是 Class 文件的标识。值是固定的,为 0xCAFEBABE。
- 如果一个 Class 文件的头四个字节不是 0xCAFEBABE,虚拟机在进行文件校验的时候会报错。使用魔数而不是文件扩展名来识别 Class 文件,主要是基于安全方面的考虑,因为文件扩展名可以随意更改。
类加载器的种类
类加载器分为四种,前三种是 JVM 自带的类加载器。特别注意,它们之间并没有继承关系,但内部都有一个 parent
属性。
- 启动类加载器(BootStrapClassLoader),由 C++ 语言实现
- 不是
Java.lang.ClassLoader
的子类 - 负责加载
$JAVA_HOME
中jre/lib/rt.jar
里所有的 Class,例如java.lang.Object
和java.lang.String
等 - 是 Java 虚拟机的一部分,而且是用本地代码实现的,不是 Java 类,因此在 Java 中无法直接引用它
- 不是
- 扩展类加载器(ExtensionClassLoader),由 Java 语言实现
- 别名叫平台类加载器(PlatformClassLoader)
- 是
Java.lang.ClassLoader
的子类,全类名为sun.misc.Launcher$ExtClassLoader
- 负责加载 Java 平台中扩展功能的一些 Jar 包,包括
$JAVA_HOME
中jre/lib/ext/*.jar
或-Djava.ext.dirs
指定目录下的 Jar 包
- 应用程序类加载器(ApplicationClassLoader),由 Java 语言实现
- 别名叫系统类加载器(SystemClassLoader)
- 负责加载
classpath
中指定的 Jar 包及目录中 Class,这些类通常是由用户编写的 - 在 Java 应用程序中,它是默认的类加载器,也是
Java.lang.ClassLoader
的子类,全类名为sun.misc.Launcher$AppClassLoader
- 自定义加载器(CustomClassLoader),由 Java 语言实现
- 是
Java.lang.ClassLoader
的子类 - 用户可以使用自定义类加载器实现特定的类加载需求,比如从网络上加载类,或者从数据库中加载类等
- 用户可以重写父类的
loadClass(String name, boolean resolve)
方法来改变类加载的行为,还可以重写父类的findClass(String name)
方法来实现自定义的类查找逻辑
- 是
类加载器的工作流程
- (1) 当 ApplicationClassLoader 加载一个 Class 时,它首先不会自己去尝试加载这个类,而是将类加载请求委派给它的父类加载器 ExtensionClassLoader。
- (2) ExtensionClassLoader 在接收到加载请求后,会继续将请求向上委派给它的父类加载器 BootStrapClassLoader。
- (3) BootStrapClassLoader 尝试加载该类,如果加载成功,则加载过程结束;如果加载失败(比如在
$JAVA_HOME/jre/lib/rt.jar
里查找不到到该 Class),则将类加载请求返回给 ExtensionClassLoader。 - (4) ExtensionClassLoader 尝试加载该类,如果加载成功,则加载过程结束;如果加载失败,则将类加载请求返回给 ApplicationClassLoader。
- (5) 最后,ApplicationClassLoader 会尝试自己加载类,如果加载失败,则会抛出
ClassNotFoundException
异常。
上述类加载器的工作流程,其本质就是双亲委派机制。简单来说:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父类加载器去完成,依次向上传递。如果父类加载器都无法完成加载任务,那么自己就会去执行加载任务。