大家好,我是三叔,很高兴这期又和大家见面了,一个奋斗在互联网的打工人。
当我们运行Java程序时,Java虚拟机(JVM)会在运行时动态加载Java类文件,这个过程被称为类加载。类加载机制是Java语言的核心特性之一,它允许Java程序在运行时动态地加载类并执行代码。在本篇博客中,我们将深入探讨JVM的类加载机制。
类加载的过程
一个类从加载到jvm内存中开始,直到被卸载,它的生命周期主要经历了三个过程:
- 加载: 主要完成三件事情
- 通过一个类的全限定名来获取此类的二进制字节流
- 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成代表这个类的class对象,作为方法区这个类的各个数据的访问入口。
- 连接:
连接又细分为:
1. 验证: 确保加载的类符合JVM规范,防止恶意代码。
2. 准备: 为类的静态变量分配内存,并设置默认的初始值。
3. 解析: 将类的符号引用转换为直接引用,即将类中的常量池中的符号引用替换为直接引用。 - 初始化
在虚拟机中进行类加载的过程中,解析这一步是可以在初始化之后再开始,加载-》验证-》准备-》初始化-》卸载,这五个阶段是顺序进行的。
类加载器
类加载的过程中,在加载这一步,获取类的二进制流这步操作是可以允许在jvm外部去实现的,实现这个动作的就是类加载器。
类加载器主要分为两类:
- 启动类加载器(Bootstrap Class Loader):底层是c++语言实现的,是jvm自身
- 自定义类加载器:负责加载应用程序的自定义类。
每个类加载器都有自己的命名空间,加载同一类时,不同的类加载器会加载出不同的类实例。这意味着同一个类在不同的类加载器中是不同的,它们之间是隔离的。
双亲委派
从Java开发角度来看,类加载器几乎都采用了双亲委派机制这种三层类加载器进行加载。当一个类加载器需要加载一个类时,它会先委派给它的父加载器去加载。如果父加载器无法加载该类,则会由该类加载器自己加载。
这种机制可以保证Java核心类库的安全性和一致性。因为所有的类都是由引导类加载器加载,所以Java核心类库是唯一的。而自定义类加载器则可以避免类重复加载和冲突。
双亲委派模型的原理:
如果一个类加载器收到了类加载的请求,首先它不会自己去进行该类的加载,而是把这个请求委派给它的父类去加载,每一层依次向上加载,直到达到启动类加载器。只有父类加载器无法完成这个加载请求,没有找个所需的类,子类加载器才会尝试自己去完成加载。
双亲委派机制的实现
在ClassLoader类中,有这么一段源码:解释了双亲委派的实现原理
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);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果未找到类,则抛出ClassNotFoundException
// 从非空的父类装入器,父类无法完成加载
}
if (c == null) {
// 如果仍未找到,则调用findClass
// to find the class.
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) {
resolveClass(c);
}
return c;
}
}
JDK1.8之前的双亲委派模型
JDK1.8之后的双亲委派模型
扩展类加载器(Ext加载器)已经被干掉了,取而代之的是平台类加载器(大家可以了解下Oracle和IBM公司之间的爱恨情仇,这里我就不过多阐述为什么jdk1.9 Ext加载器被Platform加载器给替代了)
双亲委派机制可以打破吗?
双亲委派机制在JDK发展的历史过程中,有三次被“打破,下面主要介绍在我们开发中,最常见的打破双亲委派机制的就是:
- JDBC!不同的数据库厂商,有各自的实现方法,对于开发者,必然是需要一套spi机制来屏蔽掉差异性,Java中设计spi的加载基本上都打破了双亲委派模型的一般性原则。
- 自定义类加载器:如果自定义了一个类加载器,并在其中重写了 loadClass 方法,则可以打破双亲委派机制,自行决定是否要委托给父类加载器进行加载。
- 线程上下文类加载器:Java 中还提供了线程上下文类加载器,它可以在运行时将类加载任务委派给指定的类加载器进行加载,而不是按照双亲委派机制查找类。
在一些情况下,由于线程上下文的变化,线程可能需要使用不同的类加载器。这时,线程上下文类加载器就起到了重要的作用。
Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());
这个方法可以设置当前线程的上下文类加载器为指定的classLoader对象。在以后的类加载过程中,线程将使用该classLoader对象加载类。
最后
深入理解类加载机制是非常重要的,这可以帮助我们更好地设计和开发Java应用程序,并在面对复杂的类加载问题时更加从容应对。我们需要不断的学习,不断地积累,理论知识是技术落地的基础,技术实现是对理论知识的检验!