共计 2516 个字符,预计需要花费 7 分钟才能阅读完成。
介绍
类加载器子系统是负责加载.clss
文件的,它们在文件开头会有特定的文件标识,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构(Runtime Data),并且ClassLoader
只负责class文件的加载,至于能否允许则由执行引擎(Execution Engine)来决定。
类加载器的全过程
从类被加载到JVM内存中开始,到释放内存总共有7个步骤:加载、验证、准备、解析、初始化、使用和卸载。其中验证、准备和解析
三个部分统称为链接。而加载->链接->初始化是整个类加载器的全过程。
加载
在加载阶段中,类加载器会通过一个类的全限定名获取定义此类的二进制字节流,并将这些字节流从所代表的静态存储结构转化为方法区的运行时数据结构,并在内存中生成一个代表这个类的java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。
记载
.class
文件的方式:
- 本地系统文件直接加载
- 网络获取:Web Applet
- 压缩包中读取:jar、war包
- 运行时计算生成:动态代理技术
链接
继加载阶段之后就是链接阶段,链接阶段可再细分为:验证
、准备
和解析
。
验证
验证阶段的目的在于确保class文件的字节流中包含的信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。这其实就是一个安全检查。验证主要包括四种验证:文件格式验证(JVM要求的clas文件开头包含"CAFEBABE")、元数据验证、字节码验证、符号引用验证。
准备
在验证阶段完成后就到了准备阶段。该阶段会为static类变量分配内存并且设置该变量的默认初始值,即零值。需要注意的是:该阶段的零值初始化过程不包含用final修饰的static变量,因为final在编译的时候就会分配了,准备阶段会显式初始化,同时这里也不会为实例变量分配初始化,因为static类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
对于某类中的
private final static int a = 1;
代码,在编译阶段就被分配了,准备阶段,a会被显式地赋上了1,而不是零值。而对于private static b = 1;
代码,在准备阶段,b会赋上对应类型的零值即0,而1的赋值则在初始化阶段完成。最后对于private int c;
代码,c属于实例对象属性,其赋值需要在实例对象创建时完成,因此不在此处完成内存分配。
解析
JVM将常量池内的符号引用替换为直接引用的过程。符号引用就是用一组字符串来描述引用的目标。例如:import java.util.Scanner
就算符号引用。直接引用就是指针或对象地址,引用对象一定是在内存中进行。
初始化
初始化阶段就是执行类构造方法的<clinit>()
的过程,而且要保证执行前父类的<clinit>()
方法执行完毕。这个方法由编译器收集,顺序执行所有类变量(static 修饰的成员变量)显示初始化和静态代码块语句。static int a = 2
在准备阶段赋值为零值,而到了这个阶段才正式赋值为2。如果在该阶段中,类变量在静态代码块中又进行了更改,则会覆盖先前的显示初始化。
虚拟机保证在多线程下,一个类的
<clinit>()
方法被同步加锁。即多线程下类只会被加载一次。
public class Test {
static class Father {
private static A = 1;
static {
A = 2;
}
}
static class Son extends Father{
private static B = A;
}
public static void main(String[] args) {
System.out.println(Son.B) // 2
}
}
在上述程序执行过程中,当调用Son.B时,JVM会先尝试去加载Son类,当发现Son类继承自Father类时,又会先去加载Father类,加载Father类过程中,A在链接阶段的准备过程中将其赋值为0,然后在初始化阶段赋值为1,又因为存在static代码块,因此会执行static代码块的内容将原来的1覆盖掉,变为2,因此Father类中,A的值为2。Father加载完毕后回到Son,Son将自己的B由0赋值为2,最终打印出来的结果就是2。
类加载器的分类
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。引导类加载器是用C/C++实现的,内嵌在JVM中,而自定义类加载器都是通过继承ClassLoader
抽象类实现的,一般是Java实现的。
然而,无论我们怎么划分,程序中最常见的类加载器始终只有3种:
- Bootstrap Class Loader:加载Java的核心类,如
java.lang.*
中的类,该加载器是用C/C++实现,因此无法通过Java代码进行访问。 - Extension Class Loader:加载Java平台的扩展库,位于JRE的lib/ext目录下的类。
- Application Class Loader:系统类加载器,负责加载应用程序的类,通常从classpath中加载类文件。
根据它们的功能,不难推出这些加载器的执行顺序:
Bootstrap ClassLoader->Extension ClassLoader -> App ClassLoader -> Custom ClassLoader
双亲委派机制
Java虚拟机堆class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,JVM采用的是双亲委派机制,即把请求交由父类处理,它是一种任务委派模式。
原理
- 如果一个类加载器收到类加载的请求,它并不会自己先去加载,而是将这个加载请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,知道请求到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,若无法完成加载任务,子加载器才会尝试自己去加载。
优势
- 避免类的重复加载;
- 保护程序安全,防止核心API被随意篡改;