类加载的过程

一、加载

目的

加载(Loading)阶段是整个类加载过程中的第一个阶段,需要完成以下三件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。可通过以下方式实现:
    • ZIP 压缩包中读取,最终称为 JAR、EAR、WAR 格式的基础
    • 网络中获取,最典型应用:Web Applet
    • 运行时计算生成,多用在动态代理技术
    • 数据库中读取
    • JSP文件生成
    • 加密文件中获取,典型的防 Class 文件被反编译的保护措施
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在方法区内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。
非数组类型的加载

相对于其他阶段,非数组类型的加载阶段(加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的阶段。加载阶段既可以使用虚拟机里内置的引导类加载器来完成,也可以由用户自定义的类加载器去完成。

数组类型的加载

数组类本身不通过类加载器创建,它是由 Java 虚拟机直接在内存中动态构造出来的。但数据类的元素类型(Element Type),即数组去掉所有维度的类型,最终还是要靠类加载器来完成加载。

一个数组类 C 创建过程遵循以下规则:

  • 如果数组的组件类型(Component Type ),即数组去掉一个维度的类型(不同于元素类型),为引用类型,那就递归采用加载(Loading)阶段的三个过程去加载这个组件类型,同时数组 C 也会被标记在加载此组件类型的类加载器的类名称空间上。
  • 如果数组的组件类型不是引用类型,类如 int[] 数组的组件类型为 int,Java 虚拟机将会把数组 C 标记为与引导类加载器关联。
  • 数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,它的数组类的可访问性将默认为 public,可被所有的类和接口访问到。
存储位置

加载阶段结束后,Java 虚拟机外部的二进制流就按照虚拟机所设定的格式存储在方法区中了,方法区中的数据存储格式完全由各虚拟机自行定义。

类型数据安置在方法区中后,会在 Java 堆内存中实例化一个 java.lang.Class 类的对象,这个对象将作为程序访问方法区的类型数据的外部接口

二、验证

验证是连接阶段的第一步。

目的
  • 确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部要求。
  • 保证字节码编译运行后不会危害到虚拟机的自身安全。由于 Class 文件可以由任何途径产生,甚至在二进制编辑器中完成编写,故虚拟机需对这些字节码进行严格的校验。

验证阶段包含大量验证,从整体上看,大致会完成四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。

文件格式验证

验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。

在 HotSpot 中这一阶段可能包括以下验证点:

  • 是否以魔数 0xCAFEBABE 开头
  • 主、次版本号是否在当前 Java 虚拟机接受范围内
  • 常量池的常量中是否由不被支持的常量类型
  • CONSTANT_Utf8_info 型的常量中是否有不符合 UTF-8 编码的数据
  • Class 文件中各个部分及文件本身是否有被删除的或附加的其他信息
    ......

此阶段主要目的是 保证输入的字节流能正确地解析并存储在方法区。只有此阶段验证通过后,字节流才被允许进入 Java 虚拟机内存的方法区中进行存储,且后面三个阶段全部是基于方法区存储结构上进行。

元数据验证

对字节码描述的信息进行语义分析,以保证符合《Java 虚拟机规范》的全部要求。

在 HotSpot 中这一阶段可能包括以下验证点:

  • 这个类是否有父类(除 java.lang.Object 外,所有类都应当有父类)。
  • 这个类的父类是否继承了不允许被继承的类(被 final 修饰的类)。
  • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
  • 类中的字段、方法是否与父类产生矛盾,例如:覆盖了父类的 final 字段、出现不符合规则的方法重载。
    ......

此阶段主要目的是 对类的元数据信息进行语义校验

字节码的验证

四个过程中最复杂的一个阶段,通过数据流分析和控制流分析,确保语义是否合法且符合逻辑。

在 HotSpot 中这一阶段可能包括以下验证点:

  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。
    例如:在操作数栈放置了一个 int 类型的数据,使用时却按 long 类型来加载入本地变量表。
  • 保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
  • 保证方法体中的类型转换总是有效的。
    例如:子类对象赋值给父类数据类型,是安全的,但反过来则是危险的。甚至赋值到其他不相关数据类型,则是不合法的。

此阶段主要目的是 确保语义是否合法且符合逻辑

符号引用验证

发生在虚拟机将 符号引用转化为直接引用的时候,且在解析阶段中发生。

符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的类信息进行匹配性校验,即对类是否缺少或禁止访问它依赖的某些外部类、方法、字段等资源进行校验,无法通过验证则抛出 java.lang.IllegalAcessErrorjava.lang.IllegalAcessErrorjava.lang.IllegalAcessError 等异常。

在 HotSpot 中这一阶段可能包括以下验证点:

  • 符号引用中通过字符串描述的全限定名是否能找到对应的类。
  • 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
  • 符号引用中的类、字段、方法的可访问性(private、protected、public、)是否可被当前类访问。
    ......

此阶段主要目的是 确保解析行为能正常进行

三、准备

正式为类中定义的变量,即静态变量,进行内存分配并设置类变量初始值(零值)的阶段。这些变量都存至方法区内存中。

数据类型 int long short char byte boolean float double reference
零值 0 0L (short) 0 '\u0000' (byte) 0 false 0.0f 0.0d null

方法区的定义:
在 JDK 7 及之前,HotSpot 使用永久代实现方法区。
在 JDK 8 及之后,永久代取消,方法存放于元空间(Metaspace),类变量会随着 Class 对象一起存放在 Java 堆中。元空间仍然与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中。

四、解析

Java 虚拟机将常量池内的 符号引号 替换为 直接引用 的过程。

解析发生的时间未进行规定,只要求了在执行 anewarraycheckcastgetfieldgetstaticinstanceofinvokedynamicinvokeinterfaceinvokespecialinvokestaticinvokevirtualmultianewarraynewputfieldputstatic 等操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。

invokedynamic 外,虚拟机可对第一次解析结果进行缓存,例如:运行时直接引用常量池中的记录。并把常量标识为已解析状态,避免重复解析。

类加载过程包含:
1. 类或接口的解析
2. 字段的解析
3. 方法的解析
4. 接口方法的解析

五、初始化

类加载过程的最后一个步骤。至此,Java 虚拟机才真正开始执行类中编写的 Java 程序代码,主导权移交给应用程序。

准备阶段时,变量会赋零值,而在初始化阶段,则会根据开发人员的主观想法去进行变量初始化,即为执行 () 方法的过程,() 是由 Javac 编译器自动生成的。

特点
  • () 方法是由编译器自动收集类的所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并而成的,收集顺序由代码顺序决定。静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。

  • () 方法与类的构造函数(即实例构造器 () 方法)不同,它不需要显示地调用父类构造器,Java 虚拟机会保证在子类的 () 方法执行前,父类的 () 方法已经执行完毕。故 JVM 中第一个被执行的 () 方法的类型肯定是 java.lang.object

  • 由于父类的 () 方法先执行,意味着父类定义的静态语句块要优于子类的变量赋值操作。

  • () 方法对于类和接口来说并不是必需的,若没有静态语句块,也没变量赋值操作,可以不生成 () 方法。

  • 接口中不能使用静态语句块,但仍有变量初始化的赋值操作。因此接口与类一样都会生成 () 方法。但接口与类不同的是,接口不需要先执行父接口的 () 方法,因为只有当父接口定义的变量被使用时,父接口才会被初始化。接口实现类初始化时也一样不会执行接口的 () 方法。

  • JVM 必须保证一个类的 () 方法在多线程环境中被正确加锁同步,如果多个线程同时初始化一个类,那么只会有一个线程去执行这个类的 () 方法,其他线程需要阻塞等待。

你可能感兴趣的:(类加载的过程)