深入理解Java虚拟机(十)——类加载过程

292 阅读5分钟

类加载的全过程包括:加载、验证、准备、解析、初始化,下面介绍这5个步骤虚拟机所做的工作。

1. 加载

加载是类加载过程的一个阶段,这两个概念不要混淆。

1.1 加载阶段虚拟机所做的工作

●通过类的全限定名称获取到类的二进制字节流。

●将这个字节流的静态数据结构转化为方法区的运行时数据结构。

●为该类生成一个Java.lang.Class对象,该对象是类的访问入口。(注:在Hotspot虚拟机中,Class对象虽是对象,但它不存放在堆中,而是存放在方法区里)

1.2 获取类二进制字节流的方式

虚拟机规范并没有限定二进制字节流的获取必须来自于Class文件,准确地说虚拟机规范根本没有说明字节流的获取来源,因此字节流的获取可以有多种方式:

  • ●Class文件获取。
  • ●压缩包获取,例如JAR、WAR、EAR等。
  • ●通过动态代理和反射技术在运行时动态生成。
  • ●从网络中获取。
  • ●由其他文件生成,例如JSP。
  • ●从数据库读取,有些中间件会这么做。

1.3 数据类和非数组类在加载阶段的区别

●对非数组类而言,类的加载要靠虚拟机提供的类加载器或是自定义加载器去完成加载。

●对数组类而言,类的加载不通过类加载器,而是由虚拟机直接创建;但对于数组类的元素类型来说(去掉数组所有维度后的类型),还是要靠类加载器去加载。

2. 验证

2.1 验证的目的

验证是为了确保Class文件包含的信息符合当前虚拟机的要求,并且不会产生安全问题。

2.2 验证阶段存在的意义

Java语言是一门相对安全的语言,这是因为编译器的帮助下,Java可以做到防止访问数组边界以外的数据、防止将一个对象转型为它并未实现过的类型等,然而Java代码或者说字节码本身并不具备这样的安全检测能力。我们知道,二进制字节流的形成方式有很多种,并不一定要通过编译器编译形成,所以为了确保Class文件是符合规范要求且不产生危害的,我们需要对其进行验证。

2.3 验证阶段的检验动作

验证阶段的检验大体包括四个步骤:文件格式验证、元数据验证、字节码验证、符号引用验证。

  • 1.文件格式验证:这个阶段要验证二进制字节流是否符合Class文件格式的规范,需要说明的是,这个阶段的验证是基于二进制字节流进行的,通过验证后,字节流才会进入方法区进行存储,所以后面的验证步骤都是基于方法区的存储进行的,不会再直接操作字节流。
  • 2.元数据验证:这个阶段是对字节码存储的信息进行语义分析,保证字节码携带的信息符合Java语言规范。
  • 3.字节码验证:这个阶段是整个验证过程最复杂的一个阶段,作用是确保程序语义是合法并且不产生危害的。
  • 4.符号引用验证:这个阶段的验证发生在虚拟机将符号引用转化为直接引用的时候,目的是为了保证解析动作的正常执行。

2.4 验证阶段小结

对虚拟机的类加载机制来说,验证非常重要,但不是每次加载类都必不可少的一个环节:对于一些反复使用过的、能确保其安全性的代码,我们可以跳过验证环节以减少类加载过程的开销。将参数-Xvertify设置为none可关闭验证。

3. 准备

在准备阶段,虚拟机为类变量(static变量)分配方法区的内存并设置初始零值。此外,对于常量(public static final)来说,在准备阶段分配内存后不会初始为零值,而是初始为ConstantValue属性所指定的值。

示例一:public static String value = "ABC";
static变量value在准备阶段分配内存后被设置为初始零值null

示例一:public static final String value = "ABC";
常量value在准备阶段分配内存后被设置为ConstantValue属性所存储的值“ABC”。

4. 解析

解析阶段虚拟机将常量池中的符号引用解析为直接引用。

5. 初始化

在准备阶段,类变量已经赋过一次初始值(要么是初始零值,要么是ConstantValue属性指定的值),而在初始化阶段,类变量将根据程序员主观设置的值再次进行初始化。从另外一个角度讲,初始化阶段也可以说是类构造器方法< clinit>()的执行过程。

5.1 类构造器方法的特点

●< clinit>()方法由编译器自动收集类中的所有类变量和静态语句块中的语句后合并产生的。静态语句块只能访问定义在静态块前的静态变量,而对于定义在静态块之后的静态变量,静态块只能赋值,不能访问。

●父类的< clinit>()方法要早于子类的< clinit>()方法执行。

●< clinit>()方法对类或变量来说并不是必须的,若是类没有定义静态语句块和静态变量,那么编译器不会为其生成< clinit>()方法。

●接口不能使用静态块,但接口能够定义静态变量,因此编译器也可以为接口生成< clinit>()方法。