类的生命周期和类加载的过程
在了解类加载器之前,我们先来了解一下一个类的生命周期和类加载的过程。
一个类完整的生命周期包括 加载、验证、准备、解析、初始化、使用和卸载,一共7个阶段。
类加载过程包括,加载、连接和初始化,一共3个阶段,其中连接阶段又包含验证、准备和解析3个阶段。
如果想了解类加载过程的详细内容,请移步观看我的另一篇博客——JVM面试题详解系列——类加载过程详解
类加载器总结
从Java虚拟机角度,只存在以下两种类加载器:
- 启动类加载器(Bootstrap ClassLoader),使用c++实现,是虚拟机的一部分。
- 其他所有类加载器,使用Java实现,独立于虚拟机,且全部继承自抽象类 java.lang.ClassLoader。
从Java开发人员的角度,类加载器可以划分的更细致一些:
-
启动类加载器(Bootstrap ClassLoader):是JVM内置的类加载器,负责加载JVM自身需要的类和资源,包括Java核心类库(如rt.jar、resources.jar等)。Bootstrap ClassLoader是使用C++语言实现的,因此无法在Java代码中直接使用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
-
扩展类加载器(Extension ClassLoader):用于加载Java扩展类库(如jre/lib/ext目录下的jar包)。由Java编写,是由sun.misc.Launcher$ExtClassLoader实现的,开发者可以直接使用扩展类加载器。
-
系统类加载器(System ClassLoader):也称为应用程序类加载器(Application ClassLoader),用于加载应用程序的类文件,即classpath下的类文件。由Java编写,是由sun.misc.Launcher$AppClassLoader实现的。
Java类加载器的作用主要有以下几个方面
-
加载类文件:类加载器负责将类文件加载到JVM中,以便JVM能够执行其中的Java代码。
-
提供命名空间:类加载器为每个类提供了一个独立的命名空间,不同的类加载器加载的同名类是相互独立的,避免了类名冲突的问题。
-
实现类的隔离:类加载器可以实现类的隔离,即同一个JVM中的不同的类加载器加载的类互不干扰,可以避免由于类名冲突或版本不一致等问题导致的程序崩溃。
-
实现动态加载:类加载器可以在程序运行期间动态加载类文件,使得程序可以在运行时根据需要加载不同的类。
提高程序安全性:类加载器可以通过自定义实现,实现类文件的加密和解密,提高程序的安全性。
总之,Java类加载器是JVM中非常重要的组成部分,通过实现类的动态加载、隔离和命名空间等功能,为Java应用程序提供了良好的灵活性和安全性。
双亲委派模型
双亲委派模型介绍
每一个类都有一个对应它的类加载器。系统中的 ClassLoader 在协同工作的时候会默认使用 双亲委派模型 。
如果一个类加载器收到了类加载的请求,系统会首先判断当前类是否已经被加载过,已经被加载的类会直接返回,否则才会尝试加载。加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
每个类加载器都有一个父类加载器
每个类加载器都有一个父类加载器,如果一个类加载器的父类加载器为 null,并不代表这个类加载器没有父类加载器,而是这个类的父类加载器是BootstrapClassLoader 。
双亲委派模型实现源码分析
双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoader 的 loadClass() 中,相关代码如下所示。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) { //类没有被加载,首先会将请求委派给父类加载器完成
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false); //父类加载器不为空,调用父类加载器的loadClass()方法
} else {
c = findBootstrapClassOrNull(name); //父类加载器为空,使用启动类加载器 BootStrapClassLoader 加载。
}
} 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;
}
}
双亲委派模型的好处
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改
避免类的重复加载
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子类加载器再加载一次。
保护程序安全,防止核心API被随意篡改
保护程序安全,防止核心API被随意篡改,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心 API 库被随意篡改。
举个例子 双亲委派模型如何保护程序安全,防止核心API被随意篡改
举个例子,假设在Java程序中,我们需要使用一个名为 java.lang.Object 的类。当我们创建一个新的对象时,Java虚拟机会首先查找当前线程的类加载器是否已经加载了 java.lang.Object 类。如果没有加载,Java虚拟机会将该请求委托给父类加载器。如果父类加载器也没有加载该类,Java虚拟机会继续向上委托,直到委托到启动类加载器(Bootstrap ClassLoader)为止。只有当启动类加载器也无法加载该类时,Java虚拟机才会使用当前线程的类加载器加载该类。
双亲委派模型保证了类的安全性,因为在委托加载类的过程中,Java虚拟机会首先尝试使用启动类加载器加载类。启动类加载器只加载Java核心类库中的类,这些类都是由Java官方提供并经过严格测试的,不存在安全漏洞。如果使用自定义的类加载器加载类,由于自定义的类加载器可能会加载用户自定义的类或第三方类库,这些类可能存在安全漏洞或恶意代码。因此,通过双亲委派模型,Java程序可以保证只加载来自可信源的类,从而提高了Java程序的安全性。
自定义类加载器
除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader。如果我们要自定义自己的类加载器,同样需要继承 ClassLoader。
如何打破双亲委派模型
自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 ClassLoader 类中的 loadClass() 方法。