1. 类加载过程
Java虚拟机(JVM)的 类加载 过程是将字节码文件(.class
文件)从存储设备加载到内存,并为其创建相应的类对象的过程。类加载是Java程序运行的基础,保证了程序的动态性和安全性。JVM的类加载过程主要分为五个阶段:加载(Loading)、连接(Linking)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,连接阶段又分为验证、准备和解析三个子阶段。
1.1 加载(Loading)
在这个阶段,JVM通过类加载器将类的字节码从文件系统或网络等资源中加载到内存,并创建一个java.lang.Class
对象来表示这个类。加载阶段包括以下几步:
- 通过类的全限定名查找并读取类的字节码文件(通常是
.class
文件)。 - 将字节码数据加载到JVM的内存中,并在方法区中生成类的运行时数据结构。
- 在堆中生成
Class
对象,作为类的字节码数据的访问入口。
注意:Java的类是按需加载的,只有在程序第一次使用某个类时,才会触发类的加载过程。
1.2 连接
连接阶段的目的是确保类可以被正确使用,它分为三个子阶段:验证、准备和解析。
验证(Verification)
图引用:[1]
验证是为了确保被加载的类的字节码是符合JVM规范的,并且不会破坏JVM的安全性。主要包括:
- 文件格式验证:检查
.class
文件的格式是否符合规范。 - 元数据验证:验证类中的元数据(如类的字段、方法)是否合法。
- 字节码验证:确保类的字节码操作符号是合法的,例如跳转指令不会跳到无效位置,数据类型操作正确等。
- 符号引用验证:检查解析时,符号引用是否能够解析到实际的类、字段或方法。
准备(Preparation)
准备阶段是为类的静态变量分配内存,并将其初始化为默认值。注意,此时的初始化并不是为静态变量赋值,而只是分配内存和设置默认值(例如,int
类型的默认值是0
,boolean
的默认值是false
,引用类型的默认值是null
)。
public static int a = 10;
在准备阶段,a
的值为默认的0
,而不是10
。赋值操作将在初始化阶段进行。
解析(Resolution)
解析阶段是将类的符号引用转换为直接引用。符号引用是编译时期的一种表示方式,而直接引用是运行时的真实地址或偏移量。解析过程主要包括:
- 类或接口解析:将符号引用的类或接口解析为内存中的实际类或接口。
- 字段解析:将符号引用的字段解析为具体类的字段。
- 方法解析:将符号引用的方法解析为实际的方法地址。
- 接口方法解析:解析接口中的方法引用。
《深入理解 Java 虚拟机》7.34 节第三版对符号引用和直接引用的解释如下[1]:
解析过程可以在连接的解析阶段完成,也可以在类使用时延迟进行(例如动态绑定)。
1.3 初始化(Initialization)
初始化阶段是类加载过程中唯一一个会执行代码的阶段。在这个阶段,JVM执行类的静态代码块和静态变量的初始化赋值操作。
初始化阶段,Java 虚拟机真正开始执行类中编写的Java 程序代码,将主导权移交给应用程序。初始化阶段就是执行类构造器方法的过程。
例如:
public static int a = 10;
static {
a = 20;
}
在初始化阶段,a
将被赋值为20
。
1.4. 使用(Using)
在类初始化之后,该类可以被程序使用。程序可以通过调用类的静态方法、访问静态字段、创建类实例等方式使用类。
1.5. 卸载(Unloading)
类的卸载是指当类不再被使用时,JVM将其从内存中移除。类的卸载通常由垃圾回收器完成,当没有任何类加载器引用该类的Class
对象,并且类的实例也不再存在时,类才会被卸载。
2. 双亲委派模型
2.1 类加载器
Java的类加载器分为三大类,分别是:
-
引导类加载器(Bootstrap ClassLoader)
- 负责加载Java核心库(如
rt.jar
中的类),例如java.lang.String
、java.util.List
等。这是JVM自身实现的类加载器,位于JVM的本地代码中,开发者无法直接操作引导类加载器。 - 引导类加载器从JRE安装目录下的
lib
目录或-Xbootclasspath
指定的路径中加载类。
- 负责加载Java核心库(如
-
扩展类加载器(Extension ClassLoader)
- 加载JVM扩展库,一般是加载位于
JRE/lib/ext
目录下的类或由java.ext.dirs
系统属性指定的目录下的类。开发者可以访问扩展类加载器。 - 该加载器是由
sun.misc.Launcher$ExtClassLoader
实现的。
- 加载JVM扩展库,一般是加载位于
-
应用程序类加载器(Application ClassLoader)
- 加载应用程序类路径(
CLASSPATH
)中的类,负责加载用户编写的代码。开发者也可以自定义类加载器来代替应用程序类加载器。 - 该加载器是由
sun.misc.Launcher$AppClassLoader
实现的。它是我们编写的Java应用程序最常使用的类加载器。
- 加载应用程序类路径(
除了 BootstrapClassLoader
是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader
抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。
每个 ClassLoader
可以通过getParent()
获取其父 ClassLoader
,如果获取到 ClassLoader
为null
的话,那么该类是通过 BootstrapClassLoader
加载的
2.2 双亲委派模型
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最 终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无 法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoader
的 loadClass()
中,相关代码如下所示。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先,检查该类是否已经加载过
Class c = findLoadedClass(name);
if (c == null) {
//如果 c 为 null,则说明该类没有被加载过
long t0 = System.nanoTime();
try {
if (parent != null) {
//当父类的加载器不为空,则通过父类的loadClass来加载该类
c = parent.loadClass(name, false);
} else {
//当父类的加载器为空,则调用启动类加载器来加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//非空父类的类加载器无法找到相应的类,则抛出异常
}
if (c == null) {
//当父类加载器无法加载时,则调用findClass方法来加载该类
//用户可通过覆写该方法,来自定义类加载器
long t1 = System.nanoTime();
c = findClass(name);
//用于统计类加载器相关的信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//对类进行link操作
resolveClass(c);
}
return c;
}
}
双亲委派模型的优点
1. 避免类的重复加载:同一个类只会由一个类加载器加载一次,避免了类的重复加载,保证了类加载的唯一性。
2.保证Java核心类的安全:如果允许自定义类加载器加载和替换Java核心类库中的类,例如
java.lang.String
,开发者可以定义一个具有相同全限定名(包名和类名)的自定义类。这样,当系统中调用String
类时,可能会不小心使用开发者自定义的String
类,导致系统行为出现不一致,甚至引发严重的安全问题。为了防止这种情况,Java采用双亲委派机制。根据双亲委派模型,所有类加载器在尝试加载类时,首先会委派给父加载器,最终到达最顶层的引导类加载器。引导类加载器专门负责加载JDK中的核心类库,并确保这些核心类库的加载不可被覆盖。
3. 破坏双亲委派模型
自定义加载器的话,需要继承 ClassLoader
。
如果我们不想打破双亲委派模型,就重写 ClassLoader
类中的 findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。
但是,如果想打破双亲委派模型则需要重写 loadClass()
方法。这是因为:类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()
方法来加载类)。
例如,Tomcat中的类加载器架构打破了双亲委派模型,每个Web应用都有自己的类加载器,并且每个应用之间的类加载是相互隔离的。
引用:
[1]JavaGuide(javaguide.cn):https://javaguide.cn/java/jvm/class-loading-process.html
[2]《深入理解 Java 虚拟机》
[3]Chapter 5. Loading, Linking, and Initializing:
Chapter 5. Loading, Linking, and Initializing