一、类加载时机
在Java中,类加载(Class Loading)是一个将类的.class文件中的二进制数据读入到JVM中,并为其生成对应的java.lang.Class
对象的过程。这个过程是Java运行时环境(JRE)的一部分,由类加载器(Class Loader)负责执行。关于类加载的时机,Java虚拟机规范严格规定了何时必须立即对类进行初始化(初始化是类加载过程的一个阶段),但并没有强制要求JVM在什么时候进行加载和连接(这两个是类加载过程中的前两个阶段)。不过,可以总结一些常见的类加载时机:
显式加载:当程序中显式地通过
Class.forName()
方法,或者ClassLoader
的loadClass()
方法加载某个类时,会触发该类的加载。创建类的实例:当使用
new
关键字创建类的实例时,如果该类还没有被加载和初始化,则会先触发类的加载和初始化。访问类的静态变量或静态方法:当访问一个类的静态变量(除了被声明为常量
final
并用编译时常量表达式初始化的变量),或者调用类的静态方法时,如果该类还没有被加载和初始化,则会先触发类的加载和初始化。使用反射:当通过反射API来访问类的属性、方法或构造器时,如果该类还没有被加载,则会先触发类的加载。
初始化子类:当一个类被初始化时,如果其父类还没有被初始化,则会先触发其父类的加载和初始化。
JVM启动时被指定为启动类的类:当JVM启动时,通过命令行参数(如
java MyClass
)指定的主类(启动类)会被加载和初始化。动态语言支持:某些Java框架或库,如JSP页面加载、JDBC的数据库驱动加载等,在运行时可能会动态地加载类。
二、类加载过程
Java虚拟机规范并没有规定在类加载过程中的加载(Loading)和链接(Linking,包括验证Verification、准备Preparation、解析Resolution)阶段的具体时机,这些阶段可能在不同的时间点发生,只要它们在初始化(Initialization)阶段之前完成即可。
Java的类加载(Class Loading)过程是指将类的.class文件(或者说是字节码)动态地加载到JVM(Java虚拟机)的运行时数据区(Runtime Data Area),并生成对应的java.lang.Class
对象的过程。这个过程是Java实现动态绑定和多态性的基础。Java的类加载过程大致可以分为以下几个步骤:
1. 加载(Loading)
这是类加载过程的第一个阶段。在这一阶段,JVM的类加载器(ClassLoader)通过类的全限定名(包括包名和类名)来查找并加载类的字节码文件。这个过程通常涉及到从文件系统、网络、数据库等位置读取类的字节码文件。一旦找到并加载了类的字节码,JVM就会在内存中创建一个表示该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
3. 链接(Linking)
链接阶段包含三个子过程:验证(Verification)、准备(Preparation)和解析(Resolution)。
验证(Verification):确保被加载的类的正确性,符合JVM规范,没有安全危害。
准备(Preparation):为类的静态变量分配内存,并设置默认的初始值(注意,这里只是分配内存并设置默认初始值,而不是初始化)。
解析(Resolution):将类、接口、字段和方法的符号引用(Symbolic Reference)替换为直接引用(Direct Reference)。解析可以在初始化之前完成,也可以在初始化之后完成,这取决于具体实现。
3. 初始化(Initialization)
这是类加载过程的最后一步。在这一阶段,JVM才真正开始执行类中定义的Java程序代码(也就是字节码)。这个阶段主要执行类构造器<clinit>()
方法中的代码,这个方法是由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。<clinit>()
方法对于类或接口来说并不是必需的,如果一个类中没有静态代码块,也没有静态变量的赋值操作,那么编译器可以不为这个类生成<clinit>()
方法。
注意:在JVM中,类的加载、链接(包括验证、准备、解析)和初始化都是在程序运行期间完成的,这个过程也称为类的动态加载(Dynamic Loading)。这个过程是JVM规范的一部分,不同的JVM实现(如HotSpot)可能会有所不同,但总体上遵循上述流程。
三、类加载器
Java中的类加载器(ClassLoader)是负责加载类的关键组件,JVM提供了三种类加载器:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)和应用类加载器(Application ClassLoader),这些类加载器之间存在父子关系,共同构成了Java的类加载体系。
引导类加载器(Bootstrap ClassLoader):这是虚拟机自带的类加载器,负责加载Java的核心类库,如
java.lang.*
、java.util.*
等。这些类库位于<JAVA_HOME>/lib/rt.jar
或类似路径的jar包中,但它不是Java类库的一部分,而是由JVM自身实现的。扩展类加载器(Extension ClassLoader):负责加载Java的扩展库,这些库位于
<JAVA_HOME>/lib/ext
目录下或者由系统属性java.ext.dirs
指定的位置。扩展类加载器是Java平台的一部分,通常由引导类加载器加载。应用程序类加载器(Application ClassLoader):也称为系统类加载器(System ClassLoader),它是
ClassLoader
类的一个实例,负责加载用户类路径(CLASSPATH
)上指定的类库,即java -cp
或java -classpath
所指定的路径中的类库。它是扩展类加载器的子类。自定义类加载器:用户可以通过继承java.lang.ClassLoader类来创建自己的类加载器。自定义类加载器主要用于加载网络上的类文件、从一个特定的数据库中加载类、从加密或压缩的文件中加载类、实现类隔离等。
四、双亲委派机制
Java中的类加载双亲委派机制是Java类加载器在加载类时采用的一种重要机制,它确保了类的有序加载、唯一性以及系统的安全性和稳定性。以下是关于Java类加载双亲委派机制的详细解释:
1. 基本概念
双亲委派机制(Parent Delegation Mechanism)是指当一个类加载器(ClassLoader)收到类加载的请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。每一层的类加载器都是如此,因此所有的加载请求最终都会传送到顶层的启动类加载器(Bootstrap ClassLoader)。如果父类加载器无法完成这个加载请求(即在其搜索范围中没有找到所需的类),那么子类加载器才会尝试自己去加载。
2. 工作原理
双亲委派机制的工作原理可以概括为以下几个步骤:
启动类加载器尝试加载:当一个类需要被加载时,首先由最顶层的启动类加载器尝试加载。启动类加载器主要负责加载Java的核心库,如
java.lang.*
、java.util.*
等。父类加载器尝试加载:如果启动类加载器无法加载该类,则请求会传递给下一层的类加载器,即扩展类加载器(Extension ClassLoader)。扩展类加载器负责加载Java的扩展库,一般位于
<JAVA_HOME>/lib/ext
目录下。子类加载器尝试加载:如果扩展类加载器也无法加载该类,那么请求会进一步传递给应用程序类加载器(Application ClassLoader),也称为系统类加载器。它负责加载用户类路径(
CLASSPATH
)上指定的类库。自定义类加载器尝试加载:如果应用程序类加载器也无法加载该类,并且存在自定义的类加载器,那么自定义的类加载器会尝试加载该类。
抛出异常:如果以上所有类加载器都无法加载该类,那么会抛出
ClassNotFoundException
异常。源码:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 实现委派
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
3. 作用与意义
双亲委派机制在Java中发挥着重要作用,具体体现在以下几个方面:
避免类的重复加载:通过双亲委派机制,可以确保一个类只会被加载一次,无论这个请求是由哪个类加载器发起的。这样可以避免类的重复加载,节省内存空间。
保护Java核心库的安全:由于Java核心库是由启动类加载器加载的,它们拥有最高的信任级别。通过双亲委派机制,可以防止恶意代码篡改核心类库,从而保护Java程序的安全。
实现类的隔离:每个类加载器都有自己的命名空间,可以加载自己命名空间内的类,但不能加载其他命名空间的类。这样,即使不同的类加载器加载了同名的类,也不会产生冲突。
提高系统的可扩展性:通过自定义类加载器,可以实现类的动态加载和卸载,提高系统的可扩展性。同时,自定义类加载器还可以用于加载网络上的类文件、从加密或压缩的文件中加载类等特殊场景。
4.SPI机制打破双亲委派
SPI(Service Provider Interface),是一种服务发现机制,它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类,如JDBC驱动。
如上图,SPI核心类定义在rt.jar中(如java.lang.Driver接口),所以本身是由启动类加载器加载,当调用SPI接口的实现类时,启动类加载器是无法加载实现类的,这个时候就提供了线程上下文类加载器(Thread Context ClassLoader)加载实现类,ThreadContextClassLoader是可以通过java.lang.Thread#setContextClassLoader方法设置,如果没有设置默认为ApplicationClassLoader,这样双亲委派模型中ApplicationClassLoader->BootStrapClassLoader的委派,变成了BootStrapClassLoader->ApplicationClassLoader的委派,这样就打破了双亲委派的类加载模式。
5. 总结
Java中的类加载双亲委派机制是一种重要的类加载机制,它确保了类的有序加载、唯一性以及系统的安全性和稳定性。通过双亲委派机制,Java实现了类的动态加载和隔离,提高了系统的可扩展性和灵活性。
评论区