目录
[1. 讲一下类加载过程?](#1. 讲一下类加载过程?)
[2. Java创建对象的过程?](#2. Java创建对象的过程?)
[3. 对象的生命周期?](#3. 对象的生命周期?)
[4. 类加载器有哪些?](#4. 类加载器有哪些?)
[5. 双亲委派模型的作用(好处)?](#5. 双亲委派模型的作用(好处)?)
[6. 讲一下类的加载和双亲委派原则?](#6. 讲一下类的加载和双亲委派原则?)
[7. 双亲委派模型的执行流程?](#7. 双亲委派模型的执行流程?)
[8. 打破双亲委派模型的方法?](#8. 打破双亲委派模型的方法?)

1. 讲一下类加载过程?
类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。

加载:
- 通过全类名 获取定义此类的二进制字节流。
- 将字节流 所代表的静态存储结构 转换为方法区的运行时数据结构。
- 在内存 中生成一个代表该类的**
Class
对象** ,作为方法区这些数据的访问入口。
验证:
确保 class
文件中的字节流包含的信息,符合当前虚拟机的要求,保证这个被加载的 class
类的正确性,不会危害到虚拟机的安全。验证阶段大致会完成以下四个阶段的检验动作:文件格式校验、元数据验证、字节码验证、符号引用验证。
准备:
为类中的静态字段分配内存,并设置默认的初始值, 比如int类型初始值是0。被final修饰的static字段不会设置,因为final在编译的时候就分配了。
解析:
解析阶段是虚拟机将常量池的**「符号引用」** 直接替换为**「直接引用」**的过程。符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用的时候可以无歧义地定位到目标即可。直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,直接引用是和虚拟机实现的内存布局相关的。如果有了直接引用,那引用的目标必定已经存在在内存中了。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7 类符号引用进行。
初始化:
初始化是整个类加载过程的最后一个阶段,初始化阶段简单来说就是执行类的构造器方法(()), 要注意的是这里的构造器方法()并不是开发者写的,而是编译器自动生成的。
使用:
使用类或者创建对象
卸载:
卸载类即该类的 Class 对象被 GC。
卸载类需要满足 3 个要求:
- 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被 GC
所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。
2. Java创建对象的过程?

- 类加载检查: 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池 中定位到一个类的符号引用 ,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。 如果没有,那必须先执行相应的类加载过程。
- 分配内存: 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小 在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。
- **初始化零值:**内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
- 进行必要设置,比如对象头: 初始化零值完成之后,虚拟机要对对象进行必要的设置, 例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。这些信息存放在**对象头中。**另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
- 执行 init 方法: 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始------构造函数,即 class 文件中的 < init >方法还没有执行,所有的字段都还为零,对象需要的其他资源和状态信息还没有按照预定的意图构造好。所以一般来说,执行 new 指令之后会接着执行 < init >方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全被构造出来。
3. 对象的生命周期?
对象的生命周期包括创建、使用和销毁三个阶段:
- 创建:对象通过关键字
new
在堆内存中被实例化,构造函数被调用,对象的内存空间被分配。 - 使用:对象被引用并执行相应的操作,可以通过引用访问对象的属性和方法,在程序运行过程中被不断使用。
- 销毁:当对象不再被引用时,通过垃圾回收机制自动回收对象所占用的内存空间。垃圾回收器会在适当的时候检测并回收不再被引用的对象,释放对象占用的内存空间,完成对象的销毁过程。
4. 类加载器有哪些?

- 启动类加载器(Bootstrap ClassLoader) :这是最顶层的类加载器,负责加载Java的核心库(如位于
jre/lib/rt.jar
中的类),它是用C++编写的,是JVM的一部分。启动类加载器无法被Java程序直接引用。 - 扩展类加载器(Extension ClassLoader) :它是Java语言实现的,继承自ClassLoader类,负责加载Java扩展目录(
jre/lib/ext
或由系统变量Java.ext.dirs
指定的目录)下的jar
包和类库。扩展类加载器由启动类加载器加载,并且父加载器就是启动类加载器。 - 系统类加载器(System ClassLoader)/应用程序类加载器(Application ClassLoader) :这也是Java语言实现的,负责加载用户类路径(
ClassPath
)上的指定类库,是我们平时编写Java程序时默认使用的类加载器。系统类加载器的父加载器是扩展类加载器。它可以通过ClassLoader.getSystemClassLoader()
方法获取到。 - 自定义类加载器(Custom ClassLoader) :开发者可以根据需求定制类的加载方式,比如从网络加载
class
文件、数据库、甚至是加密的文件中加载类等。自定义类加载器可以用来扩展Java应用程序的灵活性和安全性,是Java动态性的一个重要体现。
这些类加载器之间的关系形成了双亲委派模型,其核心思想是当一个类加载器收到类加载的请求时,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中。只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
5. 双亲委派模型的作用(好处)?
- 保证类的唯一性:通过委托机制,确保了所有加载请求都会传递到启动类加载器,避免了不同类加载器重复加载相同类的情况,保证了 Java 核心类库的统一性,也防止了用户自定义类覆盖核心类库的可能。
- 保证安全性:由于 Java 核心库被启动类加载器加载,而启动类加载器只加载信任的类路径中的类,这样可以防止不可信的类假冒核心类,增强了系统的安全性。例如,恶意代码无法自定义一个 Java.lang.System 类并加载到 JVM 中,因为这个请求会被委托给启动类加载器,而启动类加载器只会加载标准的 Java 库中的类。
- 支持隔离和层次划分:双亲委派模型支持不同层次的类加载器服务于不同的类加载需求,如应用程序类加载器加载用户代码,扩展类加载器加载扩展框架,启动类加载器加载核心库。这种层次化的划分有助于实现沙箱安全机制,保证了各个层级类加载器的职责清晰,也便于维护和扩展。
- 简化了加载流程:通过委派,大部分类能够被正确的类加载器加载,减少了每个加载器需要处理的类的数量,简化了类的加载过程,提高了加载效率。
6. 讲一下类的加载和双亲委派原则?
Java 的类加载过程分为三个主要步骤:加载、链接、初始化。
-
加载阶段(Loading):Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象)。这里的数据源可能是各种各样的形态,如 jar 文件、class 文件,甚至是网络数据源等;如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。
-
第二阶段是链接(Linking),这是核心的步骤,简单说是把原始的类定义信息平滑地转化入 JVM 运行的过程中。这里可进一步细分为三个步骤:
- 验证(Verification):这是虚拟机安全的重要保障,JVM 需要核验字节信息是否符合 Java 虚拟机规范的,否则就被认为是 VerifyError,这样就防止了恶意信息或者不合规的信息危害 JVM 的运行。验证阶段有可能触发更多 class 的加载。
- 准备(Preparation):创建类或接口中的静态变量,并初始化静态变量的初始值。但这里的"初始化"和下面的显式初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执行更进一步的 JVM 指令。
- 解析(Resolution):在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用。
-
最后是初始化阶段(initialization):这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑。编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。
再来谈谈双亲委派模型,简单说就是当类加载器(Class - Loader)试图加载某个类型的时候,除非父加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做。使用委派模型的目的是避免重复加载 Java 类型。
7. 双亲委派模型的执行流程?
java
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;
}
}
每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。
结合上面的源码,简单总结一下双亲委派模型的执行流程:
- 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
- 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器
loadClass()
方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器BootstrapClassLoader
中。 - 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的
findClass()
方法来加载类)。 - 如果子类加载器也无法加载这个类,那么它会抛出一个
ClassNotFoundException
异常。
JVM 判定两个 Java 类是否相同的具体规则 :JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个
Class
文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同。
8. 打破双亲委派模型的方法?
1. 通过线程上下文类加载器(TCCL)逆向委托
在基础框架需要调用用户实现类 的场景(如JNDI、JDBC),Bootstrap加载器无法识别应用层类,需逆向委派给下层加载器:
-
实现方式 :通过
Thread.currentThread().setContextClassLoader()
设置线程上下文加载器(通常为AppClassLoader
)。 -
关键点 :由高层级加载器(如Bootstrap)主动获取TCCL并加载用户类,打破自下而上的委派顺序。
-
典型场景 :JDBC驱动加载中
DriverManager
(由Bootstrap加载)通过TCCL加载com.mysql.cj.jdbc.Driver
。
2. 实现自身类加载逻辑(覆盖loadClass方法)
直接重写ClassLoader.loadClass()
方法,跳过父类委托机制:
-
步骤 :在自定义类加载器中不调用
super.loadClass()
,改为优先从自身路径加载类。 -
应用场景:热部署框架(如Tomcat)需隔离不同Web应用:
-
WebAppClassLoader 先尝试独立加载
/WEB-INF/
下的类,失败后才委派父加载器(SharedClassLoader)。 -
避免应用间类冲突,同时共享核心库(如Servlet API)。
-