类加载过程

        我们知道我们写的程序经过编译后成为了.class文件,.class文件中描述了类的各种信息,最终都需要加载到虚拟机之后才能运行和使用。而虚拟机如何加载这些.class文件?.class文件的信息进入到虚拟机后会发生什么变化呢?

一、类使用的七个阶段

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

类加载过程_第1张图片

        图中,加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地 开始,而解析阶段不一定:它在某些情况下可以初始化阶段之后在开始,这是为了支持Java语言的运行时绑定(也 称为动态绑定)。接下来讲解加载、验证、准备、解析、初始化五个步骤,这五个步骤组成了一个完整的类加载过 程。使用没什么好说的,卸载属于GC的工作 

二、加载

加载是类加载的第一个阶段。有两种时机会触发类加载:

1.预加载

        虚拟机启动时加载,加载的是JAVA_HOME/lib/下的rt.jar下的.class文件,这个jar包里面的内容是程序运行时非常常 常用到的,像java.lang.*、java.util. java.io. 等等,因此随着虚拟机一起加载。

2.运行时加载

        虚拟机在用到一个.class文件的时候,会先去内存中查看一下这个.class文件有没有被加载,如果没有就会按照类的全限定名来加载这个类。

加载阶段做了有三件事情:

(1)获取.class文件的二进制流;

(2)将类信息、静态变量、字节码、常量这些.class文件中的内容放入方法区中;

(3)在内存中生成一个代表这个.class文件的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。一般这个Class是在堆里的,不过HotSpot虚拟机比较特殊,这个Class对象是放在方法区中的;

虚拟机规范对这三点的要求并不具体,因此虚拟机实现与具体应用的灵活度都是相当大的。例如第一条,根本没有 指明二进制字节流要从哪里来、怎么来,因此单单就这一条,就能变出许多花样来:

(1)从zip包中获取,这就是以后jar、ear、war格式的基础;

(2)从网络中获取,典型应用就是Applet;

(3)运行时计算生成,典型应用就是动态代理技术;

(4)由其他文件生成,典型应用就是JSP,即由JSP生成对应的.class文件;

(5)从数据库中读取,这种场景比较少见;

三、链接

        链接包含三个步骤: 分别是 验证Verification , 准备Preparation , 解析Resolution 三个过程。

1.验证

        连接阶段的第一步,这一阶段的目的是为了确保.class文件的字节流中包含的信息符合当前虚拟机的要求,并且不 会危害虚拟机自身的安全。

        Java语言本身是相对安全的语言(相对C/C++来说),但是前面说过,.class文件未必要从Java源码编译而来,可以 使用任何途径产生,甚至包括用十六进制编辑器直接编写来产生.class文件。在字节码语言层面上,Java代码至少从 语义上是可以表达出来的。虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节 流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。

        验证阶段将做一下几个工作,具体就不细讲了,这是虚拟机实现层面的问题:

(1) 文件格式验证;

(2) 元数据验证;

(3) 字节码验证;

(4) 符号引用验证;

2.准备

        准备阶段是正式为类变量分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配 。关于这点,有两个地方注意一下:

(1)这时候进行内存分配的仅仅是类变量(被static修饰的变量),而不是实例变量,实例变量将会在对象实例化 的时候随着对象一起分配在Java堆中;

(2)这个阶段赋初始值的变量指的是那些不被final修饰的static变量,比如"public static int value = 123",value在准备阶段过后是0而不是123,给value赋值为123的动作将在初始化阶段才进行;比如"public static final int value = 123;"就不一样了,在准备阶段,虚拟机就会给value赋值为123。

3.解析

        解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程 。来了解一下符号引用和直接引用有什么区别:

1.符号引用

        符号引用是一种定义,可以是任何字面上的含义,而直接引用就是直接指向目标的指针、相对偏移量。这个其实是属于编译原理方面的概念,符号引用包括了下面三类常量:

(1) 类和接口的全限定名;

(2) 字段的名称和描述符;

(3) 方法的名称和描述符;

2.直接引用

        直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机示例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经存在在内存中了。

四、初始化

        类的初始化阶段是类加载过程的最后一个步骤, 之前介绍的几个类加载的动作里, 除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外, 其余动作都完全由Java虚拟机来主导控 制。 直到初始化阶段, Java 虚拟机才真正开始执行类中编写的Java程序代码, 将主导权移交给应用程序。

        初始化阶段就是执行类构造器()方法的过程。 ()并不是程序员在Java代码中直接编写的方法, 它是Javac编译器的自动生成物,()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块) 中的语句合并产生的, 编译器收集的顺序是由语句在源文件中出现的顺序决定的, 静态语句块中只能访问到定义在静态语句块之前的变量, 定义在它之后的变量, 在前面的静态语句块可以赋值, 但是不能访问, 如代码清单7-5所示

 
public class TestClinit {
    static {
        i = 0; // 给变量复制可以正常编译通过
        System.out.print(i); // 这句编译器会提示“非法向前引用” 
    }
    static int i = 1; 
}

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

你可能感兴趣的:(JVM,java,开发语言)