类加载过程

JVM把类的描述数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程称为虚拟机的类加载机制。
一个类型从加载到虚拟机内存开始,到卸载出内存为止,整个生命周期将会经历:
加载、验证、准备、解析、初始化、使用、卸载。
七个阶段,验证、准备、解析三个部分统称为连接。
image.png
其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载必须按照这个顺序按部就班开始。而解析阶段则不一定,在某些情况下可以在初始化阶段之后再开始,这是为了支持运行时绑定特性(动态绑定或晚期绑定)。

1 类的加载

虚拟机规范中没有强制约束加载过程的时机,但是严格规定了初始化阶段(加载、验证、准备自然需要在此之前完成)的时机,有且只有六种情况必须立即对类进行“初始化”:

  • 1、遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发初始化阶段。能够触发这四条指令的典型Java代码场景有:
    • new: 使用new关键字进行实例化对象的时候。
    • 读取或设置一个类的静态字段(被final修饰,在编译期把结果放入常量池的静态字段除外)的时候。
    • 调用类的静态方法是
  • 2、进行反射调用时,如果类型没有被初始化,则先触发初始化。
  • 3、在初始化类是,如果其父类未被初始化,先初始化其父类
  • 4、用户指定的要执行的主类(包含main()方法的那个类),虚拟机会在启动时先初始化这个类。
  • 5、当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
  • 6、当接口中定义了默认方法(default关键字修饰的接口方法),如果这个接口的实现类发生了 初始化,那该接口要在其之前被初始化。

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

2 验证

验证阶段是为了确保Class文件的字节流中包含的信息符合虚拟机规范的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机自身的安全。

3 准备

准备阶段正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段.
类变量此时赋值为初始值,在初试化阶段才会被赋值。但是被final修饰的变量在准备阶段就可以被赋原值。

4 解析

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

符号引用:符号引用是使用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。引用的目标不一定是要已加载到虚拟机内存中的内容, 直接引用:直接引用是可以直接指向目标的指针、相对偏移量或是一个能直接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关。如果有了直接引用,那引用的目标必定已经在虚拟机内存中存在。

虚拟机规范中并未规定解析阶段发生的具体时间,只要求在执行
ane-warray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invoke-special、invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield和putstatic
这17个用于操作符号引用的字节码命令之前,先对他们所使用的符号引用进行解析,所以虚拟机可以根据需要自行判断,到底在类被加载器加载时就对常量池中的符号引用进行解析,还是等一个符号引用将要被使用前才去解析。

5 初始化

初始化阶段是类加载的最后一个步骤,知道初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。
在准备阶段,变量已经赋过一次系统要求的零值,而在初始化阶段,则会根据代码去初始化类变量和其他资源。
初试化阶段就是执行类构造器()方法的过程。()方法是Java编译器的自动生成物。()方法 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。

类加载器

类加载器只用于实现类的加载动作。对于任意一个类,都必须由加载他的类加载器和这个类本身共同确立其在虚拟机中的唯一性。每一个类加载器,都以一个独立的类名称空间。通俗一点说就是:比较两个类是否相等,只有这个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机下载,只要加载他们的类加载器不同,那这两个类就不相等。

双亲委派模型

类加载器分为以下几种:

  • 启动类加载器(BootStrap Class Loader):负责加载存放在\lib目录,或者被-XbootStrapclasspath 参数所指定路径存放的且能被虚拟机识别的类加载到虚拟机的内存中。
  • 扩展类加载器(Extension Class Loader):负责加载\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。允许开发人员将具有通用性的类库放置在ext目录里以扩展JavaSe的功能。
  • 应用程序类加载器(Application Class Loader):负责加载用户类路径(classpath)上所有的类库,同时开发者可以直接在代码中使用这个类加载器。

image.png

  • 自定义类加载器:如果用户认为有必要,还可以加入自定义的类加载器进行拓展。

图例展示的各类加载器层次关系被称为类加载器的“双亲委派模型”。除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围没有找到所需的类)时,子加载器才会尝试自己去完成加载。