LHJ's Blog

Java 类加载机制

虚拟机把描述类信息的Class文件加载到内存,并对加载进来的文件进行校验、解析、转化和初始化,最终形成虚拟机可以直接使用的Java类型的过程就是类加载。

💦 类加载过程

类的整个生命周期可以分为加载、验证、准备、解析、初始化、使用和卸载这7个过程。其中加载、验证、准备、初始化的顺序是确定的,而解析不一定在准备之后,也可以在初始化之后,主要是为了适应动态绑定的需求。

⛅ 加载

加载阶段主要完成三件事情:

1️⃣ 通过类的全限类名获取这个类的二进制流

2️⃣ 将这个字节流定义的静态存储结构定义为方法区的运行时数据结构

3️⃣ 在内存中生成一个代表这个类的java.lang.Class对象,作为访问方法区这个类的各种数据类型的入口。

需要注意的是,通过类的全限类名获取这个类的二进制流 这句话的意思并不是说只能通过类似java.lang.Object这样的类名去获取二进制流,这句话的灵活度很大,例如还可以从ZIP、WAR等压缩包中读取,也可以动态生成,如JSP需要转化为Servlet等。

⛅ 验证

加载阶段加载进来的Class文件并不一定要由java编译器生成,可以通过各种方式生成,你甚至可以用16进制的编辑器自己编写一个.class文件(如果你真的这么NB的话)。如果虚拟机对加载进来的Class文件完全信任,不进行任何的检查,那么万一这个Class的内容有误或者存在危害虚拟机的内容,那么就会导致虚拟机崩溃,因此验证这一阶段是必须的。

验证阶段主要完成4个阶段的验证操作:

1️⃣ 文件格式验证

主要验证字节码文件格式是否正确,当前字节码是否能被当前版本的虚拟机处理(例如1.8以下的虚拟机可能无无法处理1.8版本、使用了新特性的Java代码)。

​ ◼ 是否以0xCAFEBABE开头

​ ◼ 主、次版本号是否在当前虚拟机处理范围之内

​ ◼ 是否存在常量池不支持的类型

​ ◼ ……

2️⃣ 元数据验证

主要对字节码描述的信息进行语义分析,确保信息满足Java的语法规范要求。

​ ◼ 这个类是否存在父类(除java.lang.Object类外,其它的类都有父类)

​ ◼ 这个类是否继承了不允许继承的类(如被final关键字修饰的类)

​ ◼ 如果这个类不是抽象类,是否实现了父类或实现的接口中的抽象方法

​ ◼ ……

3️⃣ 字节码验证

经过第二阶段的语义分析后,第三阶段主要是对执行的方法进行分析,确保逻辑正确,而且内容不会对虚拟机造成危害。

​ ◼ 确保方法中的类型转化是有效的

​ ◼ 确保跳转指令不会跳转到方法体以外

​ ◼ ……

4️⃣ 符号引用验证

符号引用验证发生在类生命周期的第四阶段 - 解析阶段(将符号引用转化为直接引用)的时候。这一阶段的验证主要是对符号引用的引用对象进行验证,确保符号引用可以正确的匹配。

​ ◼ 符号引用根据全限类名能否找到对应的类

​ ◼ ……

⛅ 准备

准备阶段主要是对类变量赋初始零值。这里的类变量指的是使用static关键字修饰的变量,而不是普通的成员变量,而且赋值的时候赋的是初始零值而不是初始值。

例如:

1
2
3
4
public class Test {
private static int a = 123;
//在准备阶段,对类变量`a`赋初始零值之后,`a`的值为0而不是123!
}

但是也有其它的情况:如果这个类变量是一个常量,即还使用了final关键字修饰,那么准备阶段赋值的时候就是指定的值。

1
2
3
4
public class Test {
private final static int a = 123;
//在准备阶段,对类变量`a`赋初始零值之后,`a`的值为123而不是0!
}

⛅ 解析

解析阶段主要是把符号引用转换为直接引用。

符号引用:符号引用是使用一组符号来描述所引用的目标,这组符号可以是任何形式的字面量,只要能够无歧义的定位到被引用的目标即可。

直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者是一个可以简介定位到目标的句柄。

⛅ 初始化

除了在加载阶段可能会使用自定义的类加载器以外,初始化阶段虚拟机才会开始执行类中程序员写的代码,初始化阶段主要是对普通类成员变量进行初始化的过程。

💧 类加载器

加载阶段主要是把外部的Class文件加载到内存,而这一步操作是由类加载器完成的。

对于任意一个类,如果这个类分别由不同的类加载器加载进入内存,那么这两个类是不相等的。不相等包括equals()方法、isInstance()方法以及instanceof关键字。

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
package com.longhujing.jvm;

import java.io.IOException;
import java.io.InputStream;

/**
* @author longhujing
*/
public class TestLoaderTest {

public static void main(String[] args) throws ClassNotFoundException {
MyClassLoader myClassLoader = new MyClassLoader();
Object obj = myClassLoader.loadClass("com.longhujing.jvm.TestLoaderTest");
System.out.println(obj.getClass());
System.out.println(obj instanceof com.longhujing.jvm.TestLoaderTest);
}

public static class MyClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try{
String className = name.substring(name.indexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(className);
if(is == null){
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
}catch (IOException ex){
throw new ClassNotFoundException();
}
}
}

}

运行结果如图:

Java中的类加载器主要分为三类:

1️⃣ 启动类加载器:主要加载JAVA_HOME/lib目录下或-Xbootclasspath参数指定的路径中,虚拟机可以识别的类库。

2️⃣ 扩展类加载器:主要加载JAVA_HOME/lib/ext目录中的或者java.ext.dirs系统变量指定的路径中的类库

3️⃣ 应用程序类加载器:也称为系统类加载器,可以通过System.getClassLoader()获得,主要加载用户类路径上的类。

🔥 双亲委派模型

双亲委派模型是类加载器在加载一个类的时候,首先不是由当前类加载器去加载这个类,而是由这个类加载器的父加载器去加载(如果由父加载器的话),如果下一个加载器也有父加载器就一直递归向上,直到顶层的类加载器。如果父类加载器可以完成类加载,那么就由父加载器完成类加载,否则交还给子类去加载。

双亲委派模型

双亲委派模型的好处是:使用双亲委派模型可以保证,多个类加载器加载同一个类的时候,最终都是由同一个类加载器完成这个类的加载,这样一来,就可以保证例如java.lang.Object在程序中都是同一个,不会重复加载。否则如果不使用双亲委派模型,那么如果用户在用户类路径定义了一个java.lang.Object类,那么就会出现程序中有多个java.lang.Object类,将会导致程序混乱。



 评论