Back

JVM笔记之JVM类加载机制

虚拟机类加载机制

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验转换(准备)解析初始化,最终形成可以被Java虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

类从被加载到虚拟内存中开始,到卸载内存为止,它的整个生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段。
其中,验证,准备和解析三个部分统称为连接(Linking)。


类加载的过程

类加载的全过程,加载,验证,准备,解析和初始化这五个阶段。

加载

在加载阶段,虚拟机需要完成以下三件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
  • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口

Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象),这里的数据源可能是各种各样的形态,如 jar 文件、class 文件,甚至是网络数据源等

验证

这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。不同的虚拟机对类验证的实现可能有所不同,但大致上都会完成下面四个阶段的检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

  • 文件格式验证 验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
  • 元数据验证 对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。
  • 字节码验证 整个验证过程中最复杂的一个阶段,主要工作是数据流和控制流的分析。在第二阶段对元数据信息中的数据类型做完校验后,这阶段将对类的方法体进行校验分析。这阶段的任务是保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。
  • 符号引用验证 发生在虚拟机将符号引用直接转化为直接引用的时候,这个转化动作将在连接的第三个阶段-解析阶段产生。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性的校验。

准备

正式为类变量分配内存并设置类变量初始值的阶段(创建类或接口中的静态变量,并初始化静态变量的初始值),这些内存都将在方法区进行分配。

解析

解析阶段是虚拟机将常量池的符号引用转换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。

初始化

前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由Java虚拟机主导和控制。
到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。 在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者说初始化阶段是执行类构造器()方法的过程。

真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。


准备阶段 常量和不同静态变量有什么区别

public class CLPreparation {
    public static int a = 100;
    public static final int INT_CONSTANT = 1000;
    public static final Integer INTEGER_CONSTANT = Integer.valueOf(10000);
}

编译并反编译一下:

javac CLPreparation.java
javap –v CLPreparation.class
static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        100
         2: putstatic     #2                  // Field a:I
         5: sipush        10000
         8: invokestatic  #3                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        11: putstatic     #4                  // Field INTEGER_CONSTANT:Ljava/lang/Integer;
        14: return
      LineNumberTable:
        line 2: 0
        line 4: 5

普通原始类型静态变量和引用类型(即使是常量),是需要额外调用putstatic 等 JVM 指令的, 这些是在显式初始化阶段执行,而不是准备阶段调用;而原始类型常量,则不需要这样的步骤。


类加载器

类与类加载器

虚拟机设计团队把类加载阶段中的"通过一个类的全限定名来获取描述此类的二进制字节流"这个动作放到Java虚拟机外部去实现,以便让程序自己决定如何去获取所需的类。实现这个动作的代码模块被称为"类加载器"。

双亲委派模型

站在Java虚拟机的角度讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另外一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader。从Java开发人员的角度来看,类加载器还可以分得更细致一些,绝大部分Java程序都会使用到以下三种系统提供的类加载器:

  • 启动类加载器
    • 加载 jre/lib 下面的 jar 文件,如 rt.jar。
    • 即使是在开启了 Security Manager 的时候,JDK 仍赋予了它加载的程序AllPermission。
    • 通过 java -Xbootclasspath :(替换)、/a(追加)、/p(前置)
  • 扩展类加载器
    • 负责加载我们放到 jre/lib/ext/ 目录下面的 jar 包,这就是所谓的 extension 机制。
    • 可以通过设置 “java.ext.dirs”来覆盖,java -Djava.ext.dirs=your_ext_dir HelloWorld
  • 应用程序类加载器
    • 也叫系统(System)类加载器,是加载我们最熟悉的 classpath 的内容。通常来说,其默认就是 JDK 内建的应用类加载器;
    • 它同样是可能修改的,比如:java -Djava.system.class.loader=com.yourcorp.YourClassLoader HelloWorld
    • 如果我们指定了这个参数,JDK 内建的应用类加载器就会成为定制加载器的父亲,这种方式通常用在类似需要改变双亲委派模式的场景。

双亲委派模型

双亲委派模型(⾃底向上检查类是否已经加载、⾃顶向下尝试加载类),简单说就是当类加载器(Class-Loader)试图加载某个类型的时候,尽量将这个任务代理给当前加载器的父加载器去做(除非父加载器找不到相应类型)。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常

使用委派模型的目的是避免重复加载 Java类型;考虑到安全因素,避免自定义的类去替代系统类,如String。

JVM不仅要判断两个类名是否相同,⽽且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。

类加载机制有三个基本特征

双亲委派模型。

但不是所有类加载都遵守这个模型,有的时候,启动类加载器所加载的类型,是可能要加载用户代码的。
用户可以在标准 API 框架上,提供自己的实现,JDK 也需要提供些默认的参考实现。
例如,Java 中 JNDI、JDBC、文件系统、Cipher 等很多方面,都是利用的这种机制,这种情况就不会用双亲委派模型去加载,而是利用所谓的上下文加载器。

可见性,子类加载器可以访问父加载器加载的类型,但是反过来是不允许。

单一性,父加载器中加载过的类型,就不会在子加载器中重复加载。

JDK9 模块化

在 JDK 9 中,由于 Jigsaw 项目引入了 Java 平台模块化系统(JPMS),JPMS),Java SE 的源代码被划分为一系列模块。-Xbootclasspath 参数不可用了。

扩展类加载器被重命名为平台类加载器(Platform Class-Loader),而且 extension 机制则被移除。也就意味着,如果我们指定 java.ext.dirs 环境变量,或者 lib/ext 目录存在,JVM将直接返回错误!建议解决办法就是将其放入 classpath 里。

rt.jar 和 tools.jar 同样是被移除了!JDK 的核心类库以及相关资源,被存储在 jimage 文件中,并通过新的 JRT 文件系统访问,而不是原有的 JAR 文件系统。虽然看起来很惊人,但幸好对于大部分软件的兼容性影响,其实是有限的,更直接地影响是 IDE 等软件,通常只要升级到新版本就可以了。

增加了 Layer 的抽象, JVM 启动默认创建 BootLayer,开发者也可以自己去定义和实例化Layer,可以更加方便的实现类似容器一般的逻辑抽象。

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus