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

 

1.类加载时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载,验证,准备,解析,初始化,使用,卸载这七个阶段。其中验证,准备,解析三个部分统称为连接(Linking),这七个阶段的发生顺序如图:

 

java 虚拟机类加载过程 java虚拟机加载顺序_java 虚拟机类加载过程

 

注意1:加载,验证,准备,初始化,卸载这五个步骤是确定的,类的加载过程必须按照这种顺序按部就班的进行,而解析阶段不一定。

注意2:这些阶段大多数情况下都是交叉的混合式进行的,通常会在一个阶段的过程中调用激活另外一个阶段。

 

什么情况下需要开始类加载过程的第一个阶段:加载?

 

  1. 遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发器初始化。
  3. 当初始化一个类的时候,如果发现其父类没有进行初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个类。
  5. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果为方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

 

 

以上五种场景中的行为称为对一个类进行主动引用。下面三种情况则为对类进行被动引用,并不会触发类的初始化。

 

通过子类引用父类的静态字段,不会导致子类初始化

 

java 虚拟机类加载过程 java虚拟机加载顺序_java 虚拟机类加载过程_02

 

 

通过数组定义来引用类,不会触发此类的初始化。

 

java 虚拟机类加载过程 java虚拟机加载顺序_父类_03

常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

java 虚拟机类加载过程 java虚拟机加载顺序_加载_04

 

类加载全过程

 

1.加载阶段:

 

  1. “加载”是“类加载”过程的一个阶段,在加载阶段,虚拟机需要完成以下3件事情:
  2. 通过一个类的全限定名来获取定义此类的二进制字节流。
  3. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  4. 在内存中生成一个代表这个类的Java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

 

通过一个类的全限定名来获取定义此类的二进制字节流,它并没有指出二进制字节流要从一个Class文件中获取,准确的说根本没有指明要从哪里获取,怎样获取,所以,我们的“二进制字节流”的来源就很多了,比如以下几种:

 

  1. 从压缩包中获取,这很常见,最终成为日后JAR,ERA,WAR格式的基础。
  2. 从网络中获取,这种场景做典型的引用就是Applet。
  3. 运行时计算生成,这种场景使用的最多的就是动态代理技术,在java.lang.reflect.Proxy中就是用了ProxyGenerator.generateProxyClass来为特定接口所生成形式为“*$Proxy”的代理类的二进制流。
  4. 由其它文件生成,典型场景是JSP应用,即由JSP文件生成对应的Class类。
  5. 从数据库中读取,这种应用的较少


补充:

相对于类加载的其它阶段来说,一个非数组类的加载阶段(准确的说,是加载阶段中获取类的二进制字节流的动作)是开发人员可控制性最强的,因为加载阶段可以使用操作系统提供的引导类加载器来完成,也可以由用户自己定义的类加载器去完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass方法)。

 

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机自行定义。

 

加载阶段与连接阶段的部分内容是交叉进行的,加载尚未完成,连续阶段可能就已经开始了,但这些夹在加载阶段之中进行的动作仍属于连续阶段的内容,这两个阶段的开始时间仍保持着固定的先后顺序。

 

2.验证阶段:

验证是连续阶段的第一步,第一阶段的目的是为了确保Class字节的字节流中信息符合当前虚拟机的要求,不会危害虚拟机自身的安全。

 

验证阶段是非常重要的,这个阶段的严谨,直接决定了Java虚拟机是否能承受恶意代码攻击,从执行性能的角度上讲,验证阶段的工作量在虚拟机的类加载子系统中又占了相当大的一部分。

 

验证阶段大致会完成下面四个阶段的检验动作:文件格式验证,元数据验证,字节码验证,符号引用验证。

 

 

文件格式验证:这一阶段要验证字节流是否符合Class文件格式规范,并且能被当前版本虚拟机处理。这一阶段可能包括下面这些验证点:

  1. 是否以魔数0xCAFEBABE开头。
  2. 主,次版本号是否在当前虚拟机处理范围之内。
  3. 常量池的常量中是否由不被支持的类型。
  4. 指向常量的各种索引值是否有指向不存在的常量或者不符合类型的常量。
  5. Class文件中各个部分及文件本身是否有被删除或附加的其他信息。

 

元数据验证:第二阶段是对字节码描述的信息进行语义分析,以保证其描述符合Java语言规范的要求,这个阶段可能包含如下验证点:

  1. 这个类是否有父类(除了obj类外,所有类都应当有父类)。
  2. 这个类是否继承了不能被继承的类(被final修饰的类)。
  3. 这个类不是抽象类,是否实现类其父类或接口中要求实现的所有方法。
  4. 类中的字段,方法是否与父类产生冲突。

 

字节码验证:第三阶段的验证是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是否合法,符合逻辑。

  1. 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作数栈放置了一个int类型的数据,使用时却按long类型来加载到本地变量表中。
  2. 保证跳转指令不会跳转到方法体以外的字节码指令上。
  3. 保证方法体中的类型转换是有效的。

 

注意:如果一个类方法体的字节码没有通过字节码验证,那肯定有问题;但是如果一个方法体通过了字节码验证,也不能说明它一定安全。这涉及到离散数学中一个很很著名的问题“Halting Problem”:通俗一点的说法就是,不能通过程序准确的检测出程序是否存在问题。

 

符号引用验证:最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段----解析阶段发生。通常需要验证以下内容:

  1. 符号引用中通过字符串描述的全限定名是否能找到对应的类。
  2. 在指定类中是否存在符合方法字段描述符以及简单名称所描述的方法和字段。
  3. 符号引用中类,字段,方法的访问性是否可被当前对象访问。

 

对虚拟机来说,验证阶段非常重要,但不是一定必要,可以通过设置虚拟机参数来关闭它,以缩短虚拟机类加载的时间。

 

3.准备阶段:

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量使用的内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的概念需要强调一下,首先,这时候进行内存分配的仅仅是类变量(被static修饰的变量)。其次,这里说的初始值“通常状况”下是数据类型的零值。

 

4.解析阶段:

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

 

符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。

 

直接引用:直接引用可以是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。

 

对同一个符号引用进行多次解析请求是常见的事情,虚拟机实现可以将一解析进行缓存从而避免解析动作重复进行。

5.初始化阶段:

 

初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。

 

准备阶段变量已经赋过一次系统要求的值了,而在初始化阶段,则根据程序员通过制定的主管计划,去初始化变量和其他资源,或者可以从另一个角度来表述:初始化阶段时执行<clinit>()方法的过程。

 

  • <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的语句块中可以对其赋值,但是不能访问。
  • <clinit>()方法与类构造函数不同,它不需要显示的调用父类的类构造器,虚拟机保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此被虚拟机执行的<clinit>()方法肯定是obj类。
  • 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
  • <clinit>()方法对于类和接口并不是必须的,如果一个没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
  • 接口中不能使用静态语句块,但仍有变量初始化的赋值操作。因此接口与类一样都会生成<clinit>()方法。
  • 虚拟机保证一个类的<clinit>()方法在多线程环境中被正确的加锁,同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程阻塞,直到<clinit>()方法执行完毕。