JVM类加载机制

1. 简介

Java代码编译成字节码后,字节码需要通过虚拟机的加载才能运行

2. 类加载过程

类加载的全过程分为5个阶段,其中验证、准备、解析属于连接阶段

  1. 加载
  2. 验证
  3. 准备
  4. 解析
  5. 初始化

加载

加载是类加载过程的一个阶段,此阶段可以分为三步

  1. 通过一个类的全限定名(如java.lang.Object)来获取此类的二进制流
  2. 将这个字节流所代表的静态数据结构方法区运行时的数据结构
  3. 在内存中生成代表这个类的Class对象(Java虚拟机规范并没有明确规定这个对象在Java堆里,但是对于HotSpot虚拟机而言,Class对象存在于方法区中)

这个阶段是由类加载器(ClassLoader)来完成

验证

验证是连接阶段的第一步,目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求。包括四个方面的验证:

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

准备

准备阶段时正式为类变量分配内存并设置类变量初始值的阶段,初始值一般是零值,不一般的情况就是类变量用 final修饰初始值是绑定的常量值。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

初始化

  • 初始化阶段是执行类构造器()方法的过程,即静态语句块。
  • 虚拟机会保证父类的()方法先执行。而如果初始化的是接口,并不需要先执行父接口的()方法,只有在使用父接口定义的变量时才会执行。
  • 虚拟机会保证执行()方法时会正确加锁同步。

3. 类加载器

类加载的加载阶段是由类加载器来完成的。即使是同一个类,由不同的类加载器来加载,两个类也是不相等的,体现在:

  • Class对象的equals()方法
  • Class对象的isAssignableFrom方法
  • Class对象的isInstance()方法
  • 使用instanceof关键字判断对象所属的类

类加载器的分类

对于虚拟机来说只存在两种不同的类加载器:

  1. 启动类加载器(Bootstrap ClassLoader)
    • 使用C++实现,属于虚拟机自身的一部分。
    • 这个类负责加载\lib目录下,或者被-Xbootclasspath参数指定的路径,并且能够被虚拟机识别的类
    • 启动类加载器无法直接被Java程序引用
  2. 其他类加载器。使用Java实现,独立于虚拟机外部。并且继承与抽象类java.lang.ClassLoader
    • 扩展类加载器(Extension ClassLoader)
      • 这个类加载器是由sun.misc.Launcher$ExtClassLoader实现
      • 负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量指定的路径的所有类库
      • 扩展类加载器可以直接使用
    • 应用程序类加载器(Application ClassLoader)
      • 这个类加载器是由sun.misc.Launcher$AppClassLoader实现
      • 负责加载用户类路径(ClassPath)上的类库
      • 应用程序类加载器可以直接使用,同时这个类加载器也是ClassLoader类的getSystemClassLoader()静态方法的返回值

类加载器的层次关系(双亲委派模型)

双亲委派模型

下图是类加载器之间的层次关系,也称为双亲委派模型(Parents Delegation Model)。可以看出除了启动类加载器之外,其他类加载器都有自己的父加载器,但这里的层次关系是以组合来实现,而不是继承(extends)。

JVM类加载机制_第1张图片
双亲委派模型

工作方式

双亲委派模型的工作方式是:当一个类加载器收到加载类的请求时,它会先把这个加载请求委托给父类加载器,如果父类加载器不能完成加载(如在自己的搜索路径下找不到该类),这时再由自己来完成加载这个类。通过这个方法,加载请求会委托到启动加载器,如果启动类加载器完成不了再依次向下的类加载器加载。

作用

双亲委派模型的作用是所有的加载请求都最先由顶层的类加载器去完成,这样可以保证加载出来的类是同一个,不会混乱。
打个比方,我们自己实现了两个类加载器,而这个两个类加载器都不是按照双亲委派模型去实现的,而是自己去对应目录下加载类。现在我们让两个类加载器加载去加载java.lang.Object类,加载出来的两个类必定是不同的(不同的类加载器加载出来的类是不同的)。

自定义类加载器

public Class loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

protected Class loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    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;
    }
}

上面的代码是抽象类ClassLoaderloadClass方法,通过类的全限定名来加载类,其中loadClass(String name, boolean resolve)方法实现了双亲委派法。具体步骤为:

  1. 通过findLoadedClass方法检查当前类加载器是否加载过该类,加载过就直接返回该Class对象。
  2. 如果没有加载过就将该加载请求委托给父加载器,如果没有父加载器则委托给启动类加载器。
  3. 如果父加载器无法完成请求则自己使用findClass方法加载该类

所以现在自定义类加载器想依照双亲委派模型去实现可以直接继承ClassLoader类并重写findClass方法。要是不想受照双亲委派模型约束可以直接重写loadClass方法。

你可能感兴趣的