1.概述
类加载指的是把class文件载入到内存,并创建运行时数据的过程。一个类的生命周期为:
加载 连接 初始化 使用 卸载。其中的连接又可以分为三部分,验证 准备 解析。
2.类加载的时机
在以下四种情况下会触发类加载:
(1)实例化一个对象或者使用类变量或者调用类方法时,类没有被加载;
(2)使用反射,类没有被加载;
(3)初始化类时,其父类没有被加载,那么就需要加载父类;
(4)主类,即包含main入口的类
3.加载
该过程主要做两件事:
(1)使用类加载器根据全类名把class文件独到内存中
(2)创建Class对象,该对象的位置没有明确规定,可以在堆区,也可以在方法区。
4.验证
因为class文件可以来源于任何地方,我们甚至可以手动生成一个,所以虚拟机必须检测其格式是否正确,以防对虚拟机造成损害。验证主要涉及一下四方面内容:
(1)格式:魔数,版本号以及表类型
(2)元数据:语义级别,比如final的类不能被继承
(3)字节码:分析控制流,看执行会不会有问题
(4)符号引用:能否找到,可见性等
5.准备
这个阶段主要为Class实例的变量分配空间并设置初始值,注意这里的初始值全被被设置为0。与类构造方法不同。
比如static int a = 123;那么在准备阶段只是设置a的值为0,在类的构造器中,才会为其赋值123。类的构造器是什么?是虚拟机自动生成的为类变量赋值的方法,比如静态块或者显式的赋值(就像这里的123)。
6.解析
把一个class文件加载进来以后,方法体内部的变量全部是符号引用,也就是指向了常量池里的变量名字符串,此时还没有定位到内存中的其他区域。解析就是把符号引用替换为直接引用的过程。直接引用才与虚拟机的内存相关。
解析主要针对以下七种符号引用:
类或接口,字段,类的方法,接口的方法,方法类型,方法句柄和调用点限制。
下面先来看前三种
(1)类或接口,java是强类型语言,所有变量必须先声明再使用,申明的的过程就会指明类名,所有设计类型或者接口的地方都会生成一个Class_info的常量池项目。这里就是对这类符号进行解析。假设当前的类为A,A的方法内部有一个B的实例,那么虚拟机最后就需要解析这个B。
如果B不是数组类型,就会用A的类加载器按照B的全限定类名来定位B的class文件位置,进行加载,当然也要有验证准备等阶段。如果B是数字类型,就加载数组中元素的class,一旦加载完毕,还需要检测访问权限,如果没有,那么就抛出IllegalAccessError异常。最后常量池的服药引用会与B的class对象关联起来。
(2)字段,源文件中的任何字段,包括自己定义的和使用别的类的,都会生成一个Fieldref_info常量项目。要解析一个字段,首先需要解析字段所在的类或者接口,然后才能解析字段。假设该字段所在的类为C,已经解析,那么首先看C里面是否有该字段,如果有,那么结束。否则看C实现的接口内部是否有,否则再看C的父类是否有(递归),如果都没有抛出一个异常。
(3)方法,解析但是Methodref_info常量。首先解析对应的类型,假设为C。然后先看C是否有,若没有看C的父类是否有,若没有看C的父类接口或者C的接口中是否有,如果接口有,那么说明没有实现,抛出一个异常。
总体而言,解析与class文件格式息息相关,class文件格式中的常量池有14种类型的常量,class中的任何名称都会被放入常量池中,解析其实就是将对常量池中的字面量转换为直接引用的过程。到目前为止,我们就明白了类型级别的解析过程,就是把当前class中用到的全部外部的类型或者字段方法全部定位到内存中其他的Class实例。当时与变量相关的解析还没有进行。
总体而言,类加载就是把一个class文件载入内存中,然后创建一个Class实例,并且解析内部的符号的过程。
7.初始化
这一步之前,并没有执行任何程序员编写的代码,都是虚拟机自己主导的,到初始化阶段才会开始执行用户自定义的代码。这个阶段主要是为类变量赋值的,执行类的构造方法<cinit>。
那么<cinit>如何生成?
虚拟机会收集所有类变量的赋值语句以及静态块然后自动生成<cinit>,程序员并不会感知这件事。关于<cinit>,在多线程环境下是需要注意的,虚拟机会加锁,同步,保证多个线程同时初始化一个类时,只有一个线程可移植性<cinit>方法,其余的方法会被阻塞。这就是使用静态内部类实现单例的原理。