虚拟机类加载机制

471 阅读9分钟

PS:虽然最近更新频率低了,但是学习一直没有停止,共勉!

今天介绍一下 JVM 类加载器机制,主要内容如下:

  1. 概述
  2. 类加载的时机
  3. 类加载的过程
  4. 类加载器
  5. 类加载器分类
  6. 双亲委托模型

概述

JVM 把字节码(.class)文件加载到内存中,并对数据进行校验、解析和初始化,最终生成可以被 JVM 直接使用的 Java 类型,这就是 JVM 的类加载机制。

在 Java 中各种类型的加载、连接和初始化过程都是在程序运行期间完成的,这种方式会在类加载时带来一些性能开销,但是具有很高的灵活性,Java 的动态扩展的语言特性就是依赖运行期间动态加载和动态链接这个特点实现的,如插件化技术中通过自定义类加载器实现资源的加载和替换,其中就是用的 Java 语言运行期间类加载的特性。

类加载的时机

类从被加载到 JVM 内存中开始,一直到从 JVM 内存中卸载位置,类加载的生命周期如下图所示:

加载、验证、准备、初始化、卸载这五个阶段的顺序是确定的,类的解析则不一定,可能会在初始化之后再进行,这是为了支持 Java 语言的运行时绑定,在整个类加载的过程中,每一个阶段都由前一个阶段触发进行。

JVM 规范中规定了类的初始化阶段,但是加载这个阶段没有进行约束,具体由 JVM 实现自己控制,当然加载、验证、准备必须在初始化这个阶段之前完成。

那么什么情况下类开始初始化呢,JVM 严格规定了下面这些情况必须对类进行初始化:

  1. 遇到 new、getstatic/putstatic、invokestatic 指令时,如果该类没有被初始化,则需对类进行初始化,上面指令分别对应使用 new 关键字进行对象实例化、读取或设置一个静态属性、调用静态方法,具体可以使用 javap 命令查看字节码文件的实现来验证;
  2. 使用 java,lang.reflect 对类进行反射调用的时候,如果该类没有被初始化,则需对类进行初始化;
  3. 当初始化一个类的时候,如果其父类还没有进行初始化,则先进行该类父类的初始化;
  4. 当 JVM 启动时,用户指定要启动的主类,比如还有 main 方法的类,JVM 会先初始化这个类;
  5. 当使用 JDK 1.7 的动态语言支持时,如果 java.lang.invoke.MethodHandler 实例最后解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic,如果这些句柄对应的类没有进行初始化,则需先对其进行初始化,至于 MethodHandler 可以理解为反射的另一种形式。

类加载的过程

下面针对类加载的几个阶段进行具体说明。

加载

class 文件通过类加载器将其中的字节码内容加载到内存中,将这个静态数据转换成方法区中的运行时数据结构,在堆内存中生成该 class 文件对应的 java.lang.Class 对象,这个 Class 对象就是访问方法区类数据的入口。

JVM 规范并没有规定 class 文件的来源,举例如下:

  • 从 zip 包中获取,最终成为 jar、war 格式的基础。
  • 从网络中获取,典型应用就是 Applet。
  • 运行时生成,典型应用就是动态代理技术,在 java.lang.reflect.Proxy 中就是使用 ProxyGenerator.generatrProxyClass 来为特定接口生成形如 Proxy 的代理类的二进制字节流。
  • 其他文件生成、数据库中获取等。

类的加载阶段与后面的链接阶段的过程时交叉进行的,没有明确的界限,加载阶段尚未完成,链接阶段可能已经开始,但是两个阶段的开始时间还是保持着固定的先后顺序。

链接

链接包括验证、准备、解析三个阶段,

  • 验证:确保 class 文件的字节流中包含的信息符合当前虚拟机的要求,且不会危害虚拟机自身的安全,从整体来看,验证阶段主要包括文件格式验证、元数据验证、字节码验证和符号引用验证,具体验证内容可以自行查看 Java 虚拟机规范。
  • 准备:正式为类变量分配内存并设置类变量的初始值,这个初始值一般是数据类型的初始值,而不是真正代码中初始化的值,如 int 初始值就是 0,这些类变量使用的内存都将在方法区进行分配,类变量指的就是被 static 关键字修饰过的变量。
  • 解析:JVM 将常量池中的符号引用替换为直接引用,这里的符号引用就是在前面验证阶段提到的符号引用验证中的符号引用
初始化

类初始化阶段是类加载阶段的最后一步,前面的加载阶段、链接阶段除了用户自定义类加载其参与外,其余操作都是由 JVM 来完成的,初始化阶段才真正开始执行 Java 代码,也就是字节码,关于类的初始化可以了解一下几点:

  1. 初始化阶段就是执行类构造器 () 方法的过程。
  2. () 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块 statuc{} 中的语句合并产生的,编译器收集顺序和源码中语句顺序一致,如静态语句块中只能访问定义在它之前的变量,定义在它后面的变量只能复制不能访问。
  3. 初始化一个类时,如果父类还没初始化,则先进行父类的初始化。
  4. JVM 会保证一个类的 () 方法在多线程环境中被正确的加锁、同步。
  5. 当访问一个 Java 类的静态域时,只有真正声明这个类才会被初始化。

类加载器

顾名思义,类加载器(class loader) 用来加载 Java 类到 JVM 中的,所有的类加载器都是 java.lang.ClassLoader 类的一个实例,前面知道在类的加载阶段会通过类加载器来加载 class 文件,也就是可以通过一个类的全限定名来获取定义此类的二进制字节流 ,这个动作的代码实现就是类加载器的实现。

对于任意一个类,都需要加载它的类加载器和这个类本身一同确立其在 JVM 中的唯一性,每个类加载器都拥有独立的类名称空间,也就是说,两个相同的类被不同的类加载器加载后将不再相等。

类加载器分类

从 JVM 的角度来说,只存在两种不同的类加载器:

  1. 启动类加载器(Bootstrap ClassLoader):一般使用 C++ 语言实现,具体由 JVM 实现。
  2. 其他类加载器:使用 Java 语言实现,独立于 JVM 之外,且都是 java.lang.ClassLoader 的一个实例,如 Android 中的 DexClassLoader。

从 Java 开发人员的角度来说,类加载器可以分为三类:

  1. 启动类加载器(Bootstrap ClassLoader):负责加载的是 JAVA_HOME\lib 下的类库,或者被-Xbootclasspath 参数所指定的路径中的并且是 JVM 识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载),启动类加载器无法被JAVA程序直接应用。

  2. 扩展类加载器(Extension ClassLoader):这个类加载器由 sum.misc.Launcher$ExtClassLoader 实现,负责加载 JAVA_HOME\lib\ext 下的类,或者是被 java.ext.dirs 系统变量所指定的路径下的类库,可以直接使用扩展类加载器。

  3. 应用程序类加载器(Application ClassLoader):这个类加载器由 sun.misc.Launcher$AppClassLoader 实现,这个类加载器是 ClassLoader 中的getSystemClassLoader() 方法的返回值, 一般也称它为系统类加载器,负责加载用户类路径(ClassPath) 下所指定的类库, 开发者可以直接使用这个类加载器, 如果应用程序中没有自定义过自己的类加载器, 这个就是程序中默认的类加载器。

双亲委托模型

先来看一下上面类加载器的关系:

上图中展示的类加载器之间的这种层次关系, 称为类加载器的双亲委派模型( Parents Delegation Model),双亲委派模型要求除了顶层的启动类加载器外, 其余的类加载器都应当有自己的父类加载器, 这里类加载器之间的父子关系一般不会以继承(Inheritance) 的关系 来实现, 而是都使用组合(Composition) 关系来复用父加载器的代码,这种方式并不是一个强制性的约束模型, 而是 Java 设计者推荐给开发者的一种类加载器实现方式。

那么双亲委托模型的工作流程是什么呢?

当一个类加载器收到类加载的请求,首先不会去加载这个类,而是把这个类加载的请求委托给父类类加载器,以此类推,最终每个类加载的请求都会委托给启动类加载器,只有当父类类加载器无法完成该类的加载,子类加载器才会尝试自己去加载,加载过程参考如下:

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{
    synchronized (getClassLoadingLock(name)) {
        // 1. 检查是否已经加载过类
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 2. 如果没有被加载,则调用父类的类加载器加载
                    c = parent.loadClass(name, false);
                } else {
                    // 3. 如果父类的类加载器不存在,则直接使用启动类加载器进行加载
                    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();
                // 4. 父类或启动类加载器都没有加载该类,则调用自己的也就是子类类加载器的findClass犯法进行类加载
                c = findClass(name);
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

JDK 1.2 之后的 java.lang.ClassLoader 添加了一个新的 protected 方法 findClass() ,如果要自定义类加载器,则直接实现 findClass() 方法即可而不必重写 loadClass() 方法,因为 loadClass() 方法最终调用了 findClass() 方法,这样自定义的类加载器就是符合双亲委托规则的。

前面介绍了 JVM 类加载机制以及类加载器的相关知识,类加载器很好的支持了 Java 的动态扩展特性,在 Android 中也有使用,如在插件化技术中用到的 PathClassLoader、DexClassLoader 都是 ClassLoader 的间接子类,正是这种对 class 文件来源没有进行限制,基于此可以实现 App 的插件化。

更多内容见微信公众号躬行之