类加载子系统
# 类加载子系统概述
- 类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。
- ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
- 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
可以看到类加载子系统中有三个大阶段:加载阶段
、链接阶段
、初始化阶段
。这三个大阶段主要包括七个小阶段,如图所示:
# 类加载过程
先有如下代码:
public class HelloLoader {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
该类的加载过程如下:
那么对于一个类而言主要就是经过三个阶段:加载阶段
、链接阶段
、初始化阶段
,如图所示。
# 加载阶段
加载阶段主要完成三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构。
- 在内存中生成一个代表这个类的
Class
对象,作为方法区这个类的各种数据的访问入口。
# 链接阶段
1、验证阶段
验证的目标是确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段会完成四项验证:
- 文件格式验证 - 验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。
- 元数据验证 - 对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。
- 字节码验证 - 通过数据流和控制流分析,确保程序语义是合法、符合逻辑的。
- 符号引用验证 - 发生在虚拟机将符号引用转换为直接引用的时候,对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
2、准备阶段
为类变量分配内存并且设置该类变量的默认初始值,即零值(如
0
、0L
、null
、false
等)。public static int value = 123;
上面的变量value被初始化为0,而不是123.
类型 默认初始值 byte (byte)0 short (short)0 int 0 long 0L float 0.0f double 0.0 char \u0000 boolean false reference null 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
public static final int value = 123;
上面的变量value就直接被初始化为123
这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
3、解析阶段
解析阶段目标是将常量池的符号引用
替换为直接引用
的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。
- 符号引用(Symbolic References) - 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
- 直接引用(Direct Reference) - 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
# 初始化阶段
初始化阶段才真正开始执行类中的定义的 Java 程序代码。初始化,为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,主要对类变量进行初始化。
1、类初始化方式
- 声明类变量时指定初始值
- 使用静态代码块为类变量指定初始值
2、类初始化步骤
- 如果类还没有被加载和链接,开始加载该类。
- 如果该类的直接父类还没有被初始化,先初始化其父类。
- 如果该类有初始化语句,则依次执行这些初始化语句。
# 类加载器
ClassLoader
即类加载器,负责将类加载到 JVM。在 Java 虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类。
JVM 加载 class
文件到内存有两种方式:
- 隐式加载 - JVM 自动加载需要的类到内存中。
- 显示加载 - 通过使用
ClassLoader
来加载一个类到内存中。
Java中ClassLoader
是一个抽象类,其后所有的类加载器都继承自ClassLoader
。
获取ClassLoader
的途径:
// 获取当前ClassLoader
clazz.getClassLoader()
// 获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
// 获取系统的ClassLoader
ClassLoader.getSystemClassLoader()
// 获取调用者的ClassLoader
DriverManager.getCallerClassLoader()
# 类加载的分类
JVM支持两种类型的类加载器 。分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。在程序中我们最常见的类加载器有三个:启动类加载器
、扩展类加载器
、应用程序类加载器
。
# Bootstrap ClassLoader
Bootstrap ClassLoader
,即启动类加载器 ,负责加载 JVM 自身工作所需要的类。
Bootstrap ClassLoader
会将存放在 <JAVA_HOME>\lib
目录中的,或者被 -Xbootclasspath
参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。
# Extension ClassLoader
ExtClassLoader
,即扩展类加载器,这个类加载器是由 =ExtClassLoader(sun.misc.Launcher\$ExtClassLoader)
实现的。
ExtClassLoader
负责将 <JAVA_HOME>\lib\ext
或者被 java.ext.dir
系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
# AppClassLoader
AppClassLoader
,即应用程序类加载器,这个类加载器是由 AppClassLoader(sun.misc.Launcher\$AppClassLoader)
实现的。由于这个类加载器是 ClassLoader
中的 getSystemClassLoader()
方法的返回值,因此一般称为系统类加载器。
AppClassLoader
负责加载用户类路径(即 classpath
)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器
# 自定义加载器
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。
自定义加载器可以做到如下几点:
- 在执行非置信代码之前,自动验证数字签名。
- 动态地创建符合用户特定需要的定制化构建类。
- 从特定的场所取得 java class,例如数据库中和网络中。
常用的场景:
- 容器 - 典型应用:Servlet 容器(如:Tomcat、Jetty)、udf (Mysql、Hive)等。加载解压 jar 包或 war 包后,加载其 Class 到指定的类加载器中运行(通常需要考虑空间隔离)。
- 热部署、热插拔 - 应用启动后,动态获得某个类信息,然后加载到 JVM 中工作。很多著名的容器软件、框架(如:Spring 等),都使用
ClassLoader
来实现自身的热部署。
# 双亲委派机制
Java虚拟机对class文件采用的是按需加载
的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式
,即把请求交由父类处理,它是一种任务委派模式。
工作原理
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
优势
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改