序
类加载一直是java
中一个比较难以理解和解释的一部分,但是由于其强大的功能,一直在为我们的开发提供着很大的支持,如我们开发调试过程中使用到的热更新技术、生产环境中为了不停机更新而使用的热部署技术,以及我们的应用容器tomcat
等其他工具,其内部也是通过使用自定义的类加载器,而满足其独特的功能。
为何类加载可以实现上面的功能,其内部又是如何运行的?我们这篇文章主要就是理解和自定一个属于我们的类加载器,达到不停机热替换功能。
本文主要讲述类加载器相关的类加载过程和类加载器,争取使用简单的语句讲清复杂的逻辑。
一、类加载器
类加载阶段主要是将已经编译好的.class
文件(即二进制字节码文件)加载到java
虚拟机中,通过复杂的处理之后,将所有的对象、参数分配到合适的地方,如下图所示:
类加载过程
类加载主要分为五个过程(如上图),每个过程中主要做的事情有:
加载:加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class
对象,作为方法区这个类的各种数据的入口。注意这里并不是非要从一个Class
文件获取,这里既可以从ZIP
包中读取(比如从java
包和war
包中读取),也可以在运行时计算生成(动态代理),也可以由其他文件生成(比如将JSP
文件换成对应的Class
类)。
检查:这一阶段主要目的是为了确保Class
文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备:准备阶段是正式为类变量分配内存并设置类变量的出事值阶段,即在方法中分配这些变量所使用的内存空间,需要注意的是这里的初始值概念,比如定义一个变量为:
|
|
实际上变量v
在准备阶段过后的初始值为0而不是8080,将v
赋值为8080的putstatic
指令是程序被编译后,存放于构造器<client>
方法之中,这里后面会解释。但是如果将变量声明为:
|
|
那么在编译阶段会为v
生成ConstantValue
值,在准备阶段虚拟机会根据ConstantValue
属性将v
赋值为8080。
解析:解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是class
文件中的:
|
|
等类型的变量。
符号引用和直接引用
- 符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面变量形式明确定义在
java
虚拟机规范的Class
文件格式中。 - 直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用目标必定是已经在内存中存在。【句柄是一种特殊的智能指针 。 当一个应用程序要引用其他系统(如数据库、操作系统)所管理的内存块或对象时,就要使用句柄】
初始化:初始化阶段是类加载的最后一个阶段,前面的类加载阶段完成后,除了在加载阶段可以自定义类加载器之外,其他操作都是由JVM
主导。到了初始阶段,才开始真正执行类中定义的Java
程序代码。
初始化阶段是执行类构造器<client>
方法的过程,<client>
方法是由编译器自动收集类中类变量的赋值操作和静态语句块中的语句合并而成。虚拟机会保证<client>
方法执行之前,父类的<client>
方法已经执行完毕。(如果这个类中没有为静态变量赋值,也没有静态语句块,那么编译器可以不为这个类生成<client>
方法)
注意:以下几种情况是不会执行类的初始化操作:
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 定义对象数组,不会触发该类的初始化。
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
- 通过类名获取
Class
对象,不会触发类的初始化。 - 通过
Class.forName()
加载类时,如果指定参数initialize
为false
时,也不会触发类的初始化,其实这个参数就是告诉虚拟机,是否要对类进行初始化。 - 通过类加载器的默认方法
loadClass()
方法,也不会触发初始化动作。
三种类加载器
上面过程中的类加载阶段实际上是被虚拟机团队放到JVM
外部实现的,以便让程序决定如何获取所需的类,JVM
提供了3种类加载器:
类加载器
启动类加载器(BootStrap ClassLoader):负责加载 JAVA_HOME\lib
目录中的,或通过-Xbootclasspath
参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar
)的类。
扩展类加载器(ExtenSion ClassLoader):负责加载 JAVA_HOME\lib\ext
目录中的,或通过java.ext.dirs
系统变量指定路径中的类库。
应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath
)上的类库。
双亲委派模型
当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。
采用双亲委派的一个好处是比如加载位于rt.jar
包中的类java.lang.Object
,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object
对象。(如下图)
二、自定义类加载器
有时候,我们需要自己定义一个类加载器去实现自己的需求,这时候,就需要针对上面的知识进行活学活用。我们这里简单提一下自定义一个类加载器需要关注的地方,后续我们会单独写一篇文章来针对自定义的类加载器进行详细说明。
findClass和loadClass
首先来看一下loadClass()
方法的源码
|
|
loadClass()
方法是类加载机制的核心思想实现,其中主要的处理逻辑如下:
- 首先通过
Class<?> c = findLoadedClass(name);
判断一个类是否被加载过 - 如果没有被加载过,则执行
if (c == null)
中的程序,遵循双亲委派模型,首先会通过递归从父加载器开始找,直到父类加载器是BootStrap ClassLoader
为止 - 如果父加载器不能执行类加载任务,那么就交给当前的类加载器,通过执行
findClass()
方法进行加载 - 最后根据
resolve
的值,判断这个class
是否需要解析
类加载器的自定义
从上面的简单分析中,我们可以看出,如果我们仅仅想简单自定义一个类加载器,那么我们只需要实现类加载器中的findClass()
方法就行,但是如果我们除了想自己实现一个类加载器,还想破坏双亲委派模型,那么我们就需要自己去重写loadClass()
方法。