Android 面试

【面试】 Java虚拟机类加载机制和双亲委派模型

2019-08-14  本文已影响10人  xbmchina

什么是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、大数据感兴趣请长按二维码关注一波,我会努力带给你们价值。觉得对你哪怕有一丁点帮助的请帮忙点个赞或者转发哦。
关注公众号【爱编码】,小编会一直更新文章的哦。

上一篇下一篇

猜你喜欢

热点阅读