1、类加载过程
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化、最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。总体过程如下图所示(JDK 1.8):
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期将会经历加载(Loading)、验证(Verification)、准备(preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)这七个阶段,其中验证、准备、解析三个部分统称为连接(Linking),如下图所示:
1.1 加载
加载的过程主要有JVM的类加载器完成,有关JVM类加载器在第2节详细介绍。在加载阶段,虚拟机需要完成以下三件事情:
(1)通过一个类的全限定名来获取此类的二进制字节流
一个类的二进制字节流即该类的.class文件,如何获取一个类的.class文件可以通过多种方式实现,比如从ZIP包中读取、从网络传输中获取、运行时计算生成(动态代理)、从数据库中读取等。
(2)将这个字节流所代表的的静态存储结构转换为方法区的运行时数据结构
第一阶段结束后,Java虚拟机外部的二进制字节流就按照虚拟机设定的格式存储在方法区之中了,结合之前介绍的JVM内存结构可知,方法区是个逻辑概念,JDK 7之前的实现是永久代,jdk7之后将永久代存储的数据分流到元空间(MetaSpace)和堆中,其中类的相关信息就存储到了元空间中了,元空间没有使用Java的堆内存,而是使用的本地内存。
(3)在堆内存生成一个代表这个类的Class对象,作为这个类的各种数据的访问入口
这一阶段会在堆内存中生成一个该类的Class对象,一个类的Class对象作为程序调用这个类的方法和数据调用的入口。
总结一下,在加载阶段会将虚拟机之外的类的.class文件二进制流加载到虚拟机中,会生成两类数据:一类是方法区的类运行时数据结构,一类是堆内存中类的Class对象。
1.2 验证
验证是连接阶段的第一步,这一阶段的主要目的是确保class文件中的字节流包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段非常重要,这个阶段是否严谨直接决定了Java虚拟机是否能够承受恶意代码的攻击,从执行性能的角度上讲,验证阶段的工作量在虚拟机的类加载子系统中又占据了相当大的一部分。此阶段主要包含如下几部分验证:
- 文件格式验证:这一阶段主要验证字节流是否符合Class文件格式规范,并且能够被当前虚拟机处理;
- 元数据验证:这一阶段主要对字节码描述的信息进行语义分析,以保证其描述的信息符合规范;
- 字节码验证:这一阶段主要通过数据流分析和控制流分析,确定程序语义是否合法;
符号引用验证:这一阶段是对类自身以外的各类信息进行匹配性校验,检查该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段。
1.3 准备
准备阶段为类中定义的static关键字修饰的静态变量分配内存并设置静态变量的初始值,静态变量使用的内存都将在方法区中进行分配。在JDK 1.7之前,静态变量放在永久代中;JDK 1.8之后,静态变量在堆内存中存放。
注意:准备阶段进行内存分配仅是静态变量(类变量),不包含实例变量,实例变量将会在实例化时随着对象一起分配到堆内存中;
准备阶段设置静态变量的初始值,这个初始值是数据的默认类型,比如int类型的0,boolean类型的false。
1.4 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要可以唯一定位到目标即可。符号引用于内存布局无关,所以所引用的对象不一定需要已经加载到内存中。各种虚拟机实现的内存布局可以不同,但是接受的符号引用必须是一致的,因为符号引用的字面量形式已经明确定义在Class文件格式中。
直接引用(Direct References):直接引用时直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局相关,同一个符号引用在不同虚拟机上翻译出来的直接引用一般不会相同。如果有了直接引用,那么它一定已经存在于内存中了。
1.5 初始化
类初始化阶段是类加载过程的最后一步。在前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的java程序代码(字节码)。初始化对于类来说,就是执行类构造器
()方法的过程。
以下动作都会触发类的初始化:new一个类的实例;
- 访问一个类的静态属性;
- 修改一个类的静态属性;
- 调用一个类的静态方法;
- 用反射API对一个类进行调用;
-
2、类加载器
2.1 类与类加载器
2.1.1 什么是类加载器
类加载阶段,在虚拟机外部,通过一个类的全限定名来获取描述该类的二进制字节流,以便让应用程序自己决定如何获取所需的类,实现这个动作的程序被称为类加载器(Class Loader)。
从开发者角度看,类加载器分为四种: BootStrap ClassLoader:根类加载器;
- Extension ClassLoader:拓展类加载器;
- Application ClassLoader:应用程序类加载器;
-
2.1.2 四种类加载器
(1)BootStrap ClassLoader
加载位于/jre/lib目录中的或者被参数-Xbootclasspath所指定的目录下的Java核心类库,而且是Java虚拟机能够被识别的(名称不符合的类库即使放在lib目录下也不会被加载)。比如位于\jdk1.8.0_73\jre\lib目录下的rt.jar(String.class、Object.class)都是被BootStrap ClassLoader加载。
(2)Extension ClassLoader
加载位于/jre/lib/ext目录中的或者java.ext.dirs系统变量所指定的目录下的拓展类库。此加载器由sun.misc.Launcher$ExtClassLoader实现。
(3)Application ClassLoader
加载用户路径(ClassPath)上所指定的类库。此加载器由sun.misc.Launcher$AppClassLoader实现。如果应用程序中没有自定义过自己的类加载器,一般情况下Application ClassLoader就是程序中默认的类加载器。比如在程序中我们自定义一个类MyClass类,该类就是由Application ClassLoader加载的。
(4)User ClassLoader
如果以上3种类加载器无法满足用户的使用场景,用户可以通过继承Java.lang.ClassLoader类来自定义类的加载方式。2.1.3 Demo感受一下类加载器
public class JVM {
public static void main(String[] args) {
Object object = new Object();
// 打印null,即BootStrap ClassLoader
System.out.println(object.getClass().getClassLoader());
JVM jvm = new JVM();
// 打印null,即BootStrap ClassLoader
System.out.println(jvm.getClass().getClassLoader().getParent().getParent());
// 打印 sun.misc.Launcher$ExtClassLoader@16d3586,即Extension ClassLoader
System.out.println(jvm.getClass().getClassLoader().getParent());
// 打印 sun.misc.Launcher$AppClassLoader@dad5dc,即Application ClassLoader
System.out.println(jvm.getClass().getClassLoader());
}
}
说明:
Object类是\jdk1.8.0_73\jre\lib目录下的rt.jar,是Java的核心类库,由BootStrap ClassLoader加载,由于BootStrap ClassLoader是C++写的,用Java程序获取只能为null。
2.2 双亲委派模型
2.2.1 什么是双亲委派模型
2.1.2中介绍的前三种类加载器互相配合,对应用程序和jre/lib下的Java类库进行加载,如下图所示:
上图中的层次关系,也被称为加载器的双亲委派模型。双亲委派模型的工作过程是: 如果一个类加载器收到了类加载的请求, 它首先不会自己去尝试加载这个类, 而是把这个请求委派给父类加载器去完成, 每一个层次的类加载器都是如此, 因此所有的加载请求最终都应该传送到最顶层的启动类加载器中, 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类) 时, 子加载器才会尝试自己去完成加载 。
双亲委派模型总结为两点:向上逐层询问是否已加载;
- 向下逐层尝试是否可加载。
双亲委派模型在抽象类ClassLoader中的protected方法loadClass()中被实现。
2.2.2 为什么需要双亲委派模型
总结一下三点原因:
- 确保安全,避免Java核心类库被修改。毕竟是BootStrap ClassLoader加载Java核心类库,在双亲委派模型下用户自定义的加载器无法加载Java核心类库;
- 避免重复加载。比如我自己在Java.lang的package包下新建一个String类,在双亲委派模型下只会通过BootStrap ClassLoader加载/jre/lib/rt.jar包中的String类,其他的String类不会被加载;
- 保证类的唯一性。比如我自己在Java.lang的package包下新建一个String类,在双亲委派模型下只能通过BootStrap ClassLoader加载/jre/lib/rt.jar包中的String类,且会报错。
参考
Java虚拟机——类加载机制和类加载器
JUC+JVM-阳哥
JVM全套面试题教程,JVM类加载机制+JVM内存管理+JVM垃圾回收机制
面试必问的 JVM 类加载机制,你懂了吗?