在java中,jvm会加载.class文件生成class对象,只有当创建了class对象时,我们才能通过该class对象创建对象实例,这里引出类加载的过程。类加载过程主要分为3部分:加载 - 链接 - 初始化,其中链接又细分为3部分:验证 - 准备 - 解析
1.加载过程
①首先通过全类名加载对应class的二进制字节流
②将字节流对应的数据结构转换为方法区的运行时数据结构(作为方法区方法的访问入口)
③在内存中分配内存,创建出class对象(java1.8中class对象会放在堆中)
2.链接过程
①验证阶段
验证阶段是进行校验操作,保证class对象符合jvm的规范,防止加载过程出现安全问题
验证又分为4类:
文件格式验证:例如对应class文件的文件格式以cafe babe(模数)开头
元数据验证:例如该类是否继承了final修饰的父类、类中字段和方法是否与父类冲突、是否出现不合法重载
字节码验证:对程序流进行检验,保证程序能顺利执行
符号引用验证:校验符号引用中通过全类名能否定位到指定的class
②准备过程
准备过程会为类变量(静态变量)分配内存空间,并初始化零值(例如:int初始化为0,long类型初始化为0L,boolean初始化为false)。这里区别实例变量,实例变量是随着对象实例创建而进行初始化,类变量随class对象创建而初始化。
注意:按逻辑上,类变量是应该在方法区中进行分配,但是从java1.7开始,类变量以及class对象都会在堆内存中进行分配,jdk1.8的方法区实现改为了元空间,不再是永久代;字符串常量池、类变量、class对象均迁移到了堆中。如下图:
③解析过程
将常量池中的符号引用替换为直接引用,涉及类或者方法、字段、接口、接口方法等符号引用。所谓直接引用即内存中的偏移地址或者指针。
符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。在程序实际运行时,只有符号引用是不够的,举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。
举个例子:当要调用某个方法时,需要将该方法的符号引用替换为它在方法表的具体位置(即方法表中的具体偏移量)
3.初始化过程
这一过程代表开始真正执行类相关的代码,jvm会自己生成一个<clinit>方法,该方法会按照咱们自己的意愿对类变量(静态变量)进行赋值操作。当多线程情况下要执行<clinit>方法时,会进行加锁操作,因此有可能出现多线程阻塞情况,并且这种情况不容易排查。
注意:<clinit>方法是对类变量进行赋值操作,jvm会抽取静态变量的赋值语句与静态代码块中的语句,合并执行