【面试】 Java虚拟机类加载机制和双亲委派模型
什么是Java虚拟机类加载机制?
虚拟机类加载机制:虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
类加载的时机
类的生命周期是从类被加载到虚拟机的内存中,到卸载出内存为止。
类的生命周期:
加载 loading
验证 verification
准备 preparation
解析 resolution
初始化 initialization
使用 using
卸载 unloading
Java语言里,类型的加载和连接过程(连接过程包括验证、准备、解析)是在程序运行期间完成的。
加载(装载)、验证、准备、初始化和卸载这五个阶段顺序是固定的,类的加载过程必须按照这种顺序开始。
解析阶段不一定,它在某些情况下可以在初始化之后再开始,这是为了运行时动态绑定特性(JIT例如接口只在调用的时候才知道具体实现的是哪个子类)。值得注意的是:这些阶段通常都是互相交叉的混合式进行的,通常会在一个阶段执行的过程中调用或激活另外一个阶段。
类的加载过程
加载阶段
主要完成以下3件事情:
1.通过“类全名”来获取定义此类的二进制字节流
2.将字节流所代表的静态存储结构转换为方法区的运行时数据结构
3.在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口
验证阶段
这个阶段目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证:
1.文件格式验证:基于字节流验证,验证字节流是否符合Class文件格式的规范,并且能被当前虚拟机处理。
2.元数据验证:基于方法区的存储结构验证,对字节码描述信息进行语义验证。
3.字节码验证:基于方法区的存储结构验证,进行数据流和控制流的验证。
4.符号引用验证:基于方法区的存储结构验证,发生在解析中,是否可以将符号引用成功解析为直接引用。
准备阶段
仅仅为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即零值,这里不包含用final修饰的static,因为final在编译的时候就会分配了,同时这里也不会为实例变量分配初始化。类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
解析阶段
解析主要就是将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析。这里要注意如果有一个同名字段同时出现在一个类的接口和父类中,那么编译器一般都会拒绝编译。
初始化阶段
初始化阶段依旧是初始化类变量和其他资源,这里将执行用户的static字段和静态语句块的赋值操作。这个过程就是执行类构造器方法的过程。
上述过程可以使用下面的脑图来概括:
类加载器的层次结构
从Java虚拟机的角度来说,只存在两种不同的类加载器:
一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现(HotSpot虚拟机中),是虚拟机自身的一部分;
另一种就是所有其他的类加载器,这些类加载器都有Java语言实现,独立于虚拟机外部,并且全部继承自java.lang.ClassLoader。
从开发者的角度,类加载器可以细分为:
启动(Bootstrap)类加载器:负责将 Java_Home/lib下面的类库加载到内存中(比如rt.jar)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
标准扩展(Extension)类加载器:是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将Java_Home /lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
应用程序(Application)类加载器:是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,因此一般称为系统(System)加载器。
除此之外,还有自定义的类加载器,它们之间的层次关系被称为类加载器的双亲委派模型。该模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,而这种父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)。
public class Test {
public static void main(String[] args) {
System.out.println(Test.class.getClassLoader());
System.out.println(Test.class.getClassLoader().getParent());
System.out.println(Test.class.getClassLoader().getParent().getParent());
}
}
执行结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@4554617c
null
注:AppClassLoader 和 ExtClassLoader 由 Java 编写并且都是 java.lang.ClassLoader 的子类,而 BootstarapClassLoader 并非由 Java 实现而是由C++ 实现,所以打印结果为null。
自定义类加载器
public class Test {
public void say (){
System.out.println("Hello");
}
}
自定义一个类加载器,继承ClassLoader。
package com.coach.jvm;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
public class CustomClassLoader extends ClassLoader {
private final String classesDir;
public CustomClassLoader(String classesDir) {
this.classesDir = classesDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = name;
if (fileName.indexOf('.') != -1) {
fileName = fileName.replaceAll("\\.", "\\\\");
}
fileName = fileName + ".class";
try {
try (FileInputStream in = new FileInputStream(classesDir + fileName)) {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int len = 0;
while ((len = in.read(buffer)) != -1) {
out.write(buffer,0,len);
}
byte[] data = out.toByteArray();
return defineClass(name, data, 0, data.length);
}
}
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
public static void main(String[] args) throws ReflectiveOperationException{
//1. 将Test.java 编译为Test.class 后复制到 E:\classes 下,当然也可以选择其他目录作为加载目录。
//2. 加载
ClassLoader classLoader = new CustomClassLoader("E:\\classes\\");
Class<?> clazz = classLoader.loadClass("com.coach.jvm.Test");//如果你的Test在一个包内,需要加上包名,如x.y.z.Test
//3. 通过反射调用say()方法
Object instance = clazz.newInstance();
Method method = clazz.getMethod("say", null);
method.invoke(instance);//Hello
}
}
上面的代码中,我们使用了自定义的类加载器来加载Test类,并使用反射机制成功调用了Test类的方法。
双亲委派模型过程
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。
双亲委派模型的系统实现
在java.lang.ClassLoader的loadClass()方法中,先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载失败,则抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 首先,检查该Class是否已经被加载,如果已加载直接返回。
Class c = findLoadedClass(name);
// 没有被加载
if (c == null) {
long t0 = System.nanoTime();
try {
//是否存在上层加载器,如果存在交由上层加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {//如果不存在继续向上委派给BootstarapClassLoader加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//所有上层加载器都无法加载,由当前加载器进行加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
注意,双亲委派模型是Java设计者推荐给开发者的类加载器的实现方式,并不是强制规定的。
参考文章
https://www.jianshu.com/p/5f3278916b38
https://blog.csdn.net/xu768840497/article/details/79175335
最后
如果对 Java、大数据感兴趣请长按二维码关注一波,我会努力带给你们价值。觉得对你哪怕有一丁点帮助的请帮忙点个赞或者转发哦。
关注公众号【爱编码】,小编会一直更新文章的哦。