目录
一、类加载过程
1. 加载
2. 连接
a. 验证
b. 准备
c. 解析
3. 初始化
二、双亲委派模型
类加载器
双亲委派模型的工作过程
双亲委派模型的优点
一、类加载过程
JVM的类加载机制是JVM在运行时,将 .class 文件加载到内存中并转换为Java类的过程。它是Java语言实现跨平台特性的核心之一。
对于一个类来说,它的生命周期是这样的:
其中,类加载的过程主要可以分成 5 个步骤(前 5 步),中间 3 步都属于连接,所以也可以说类加载过程主要分成 3 个步骤:
1. 加载
2. 连接
- 验证
- 准备
- 解析
3. 初始化
下面我们来看每个步骤的具体执行内容:
1. 加载
“加载”阶段是整个“类加载”过程中的一个阶段,它和类加载是不同的。
加载阶段是指将类的字节码文件加载到内存中的过程。在加载 Loading 阶段,Java虚拟机需要完成以下三件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。(全限定名,例如:java.lang.String)
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的 .class 对象,作为方法区这个类的各种数据的访问入口。
2. 连接
a. 验证
确保加载的类符合JVM规范和Java语言规范,即确保读到的 .class 文件(字节码文件),是合法的格式。
文件格式如下图所示,此处就不详细介绍了。
b. 准备
给类的静态变量分配内存空间,并设置类变量的默认初始值。
比如此时有这样一行代码:
public static int value = 123;
它是初始化 value 的值为0(默认值),而非 123。
c. 解析
解析阶段是 JVM 将类、接口、字段和方法(运行时常量池)的符号引用解析为直接引用。
-
符号引用的获取: 在加载阶段和连接阶段之前,Java虚拟机会将类、接口、字段和方法的符号引用存储在运行时常量池中。解析阶段首先要做的就是从运行时常量池中获取符号引用。
-
符号引用的匹配: 获取到符号引用后,解析阶段会尝试将这些符号引用匹配到目标对象(类、接口、字段或方法)的直接引用。匹配的过程包括查找目标对象在内存中的位置以及确定访问权限等。
-
直接引用的生成: 一旦符号引用成功匹配到目标对象,解析阶段就会生成对应的直接引用。直接引用是指直接指向内存中目标对象的指针或偏移量,它能够直接在程序中被使用。
-
解析结果的存储: 解析阶段最终会将生成的直接引用存储在运行时常量池中,以便后续的使用。
3. 初始化
初始化阶段是类加载过程的最后一个阶段,主要负责执行类变量的赋值操作和静态代码块的初始化。
- 执行类变量的赋值操作,即按照程序员在代码中指定的初始值为静态变量赋值。这些值可以是程序中直接赋予的值,也可以是静态代码块中的计算结果。
- 如果类中存在静态代码块,则会按照在代码中的顺序执行静态代码块中的内容。静态代码块中可以包含任意合法的 Java 代码,用于执行一些静态初始化操作。
上述的一系列类加载过程,可以简单概况为以下内容:
-
加载(Loading): 加载阶段是指将类的字节码文件加载到内存中的过程。当程序中使用到某个类时,JVM会通过类的全限定名(Fully Qualified Name)来加载类。类加载器会根据类的全限定名在文件系统或网络中查找相应的.class文件,并将其加载到内存中。
-
连接(Linking): 连接阶段包括验证、准备和解析三个步骤:
- 验证(Verification): 确保加载的类符合JVM规范和Java语言规范,以防止恶意代码的注入。
- 准备(Preparation): 为类的静态变量分配内存空间,并设置默认初始值。
- 解析(Resolution): 将类、接口、字段和方法的符号引用解析为直接引用,以便后续执行。
-
初始化(Initialization): 初始化阶段是类加载过程的最后一个阶段,它负责执行类构造器的<clinit>方法,即对类的静态变量进行赋值操作和执行静态代码块。
二、双亲委派模型
提到类加载机制,就不得不提“双亲委派模型”。它是 Java 类加载机制中的一种设计思想,JVM的类加载机制采用的就是双亲委派模型。
类加载器
在 JVM 中,有一个重要的组件称为“类加载器”,它负责加载 Java 类文件到 JVM 中(根据类的全限定名来查找并加载对应的类文件,例如:java.lang.String)。JVM 中的类加载器默认有以下三种:
-
启动类加载器(Bootstrap Class Loader): 它是 JVM 的内置类加载器,负责加载 Java 核心类库(rt.jar)等核心类文件(标准库),是整个类加载器层次结构的顶层。由于它是用本地代码实现的,所以在 Java 中无法直接获取对其的引用。
-
扩展类加载器(Extension Class Loader): 它是用来加载 Java 平台的扩展库(ext 目录下的jar包)的类加载器。它的父类加载器是启动类加载器。通常情况下,我们可以通过
ClassLoader.getSystemClassLoader().getParent()
获取到扩展类加载器的引用。 -
应用程序类加载器(Application Class Loader): 也称为系统类加载器,它是用来加载应用程序的类文件的类加载器,它负责加载类路径(classpath)上指定的类库。它的父类加载器是扩展类加载器。通常情况下,我们可以通过
ClassLoader.getSystemClassLoader()
获取到应用程序类加载器的引用。
除了上述的三种主要的类加载器之外,JVM 还支持用户自定义的类加载器,用户可以根据需要实现自己的类加载器来加载特定的类文件。用户自定义的类加载器通常继承自 java.lang.ClassLoader
类,并重写其 findClass()
方法来实现类加载的逻辑。
双亲委派模型的工作过程
双亲委派模型的工作过程如下:
- 当一个类加载器收到类加载请求时,它不会立即尝试自己去加载这个类,而是先将这个请求委派给它的父类加载器去完成。这个过程会一直向上进行,直到达到最顶层的启动类加载器。只有当父类加载器反馈自己无法完成这个加载请求(即在其搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载这个类。
双亲委派模型的优点
上述设定有以下几个优点:
-
安全性: 双亲委派模型可以帮助保证 Java 类库的安全性。由于类加载器会先委托给父类加载器加载类,这样可以防止恶意类被加载到 JVM 中。父类加载器通常是由 JVM 提供的,是由Java官方实现,因此可以信任。这有助于防止在 Java 应用程序中意外加载不安全或有潜在安全风险的类。例如,我们在代码中自己定义了一个 java.lang.String 这样的类,根据双亲委派模型的设定,这个类会被启动类加载器找到并加载,此时加载的是Java核心类,而自定义的 java.lang.String 类实际上是不会被加载的,这就保证了Java核心类库中的类无法被替换。
-
避免重复加载类: 双亲委派模型可以避免同一个类被多次加载到 JVM 中。当一个类被加载后,它会被缓存起来,以避免重复加载。这有助于节省内存空间,并且可以确保所有代码都是基于相同的类实例运行。
-
统一性: 双亲委派模型可以确保 Java 类库的一致性。因为所有的类加载请求都会经过父类加载器,所以无论是在 Java 应用程序中还是在 Java 核心类库中,都可以保证加载的是同一个类。
双亲委派模型在生活中的类比:
- 假设你在一家公司工作,你的经理接到了一个任务,他会根据任务的性质和自己的能力来判断是否能够完成这个任务。
- 如果经理认为自己无法完成任务,他会将任务转交给更高级别的领导,如部门主管或总经理。
- 只有当更高级别的领导无法完成任务时,任务才会逐级向下转交,直到有可能被转交给你。这种机制确保了任务能够被最适合的人完成,提高了工作效率和质量。
双亲委派模型,是Java虚拟机(JVM)遵循的默认类加载机制。但也有一些方式能够打破这个机制,这里简单介绍:
- 自定义类加载器: 开发自定义类加载器可以完全改变类加载的方式。通过实现自定义的ClassLoader类,可以实现不同于双亲委派模型的加载行为。例如,可以实现一个不遵循双亲委派模型的类加载器,直接从指定的位置加载类,而不是按照双亲委派模型从上至下逐级加载。