JVM的类加载机制:JVM把描述类的数据从Class文件中加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
与其他编译时需要进行连接工作的语言不同,在Java语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种做***增加性能开销,但为Java应用程序提供了高度的灵活性。

类加载时机

类从被加载到JVM内存中开始到卸载出内存,它的生命周期包括:加载、验证、准备、解析、初始化、只用、卸载7个阶段。
验证、准备、解析三部分被称为连接。类的生命周期如下图所示:

加载、验证、准备、初始化和卸载这5个阶段顺序是确定的,解析阶段则不一定:某些情况下可以在初始化阶段之后再开始,这么做是为了支持Java语言的运行时绑定(动态绑定)

Java虚拟机规范没有强制约束JVM什么时候开始类加载过程的第一个阶段:加载,所以这由JVM的具体实现决定。
Java虚拟机规范严格规定了只有五种情况必须进行类的初始化(加载、验证、准备必在这之前开始):

  • 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行初始化,则需要先进行初始化
  • 使用java.lang.reflect包的方法对类进行反射调用时,如果类没有进行初始化,则需要先进行初始化
  • 当初始化一个类的时,其父类未被初始化,则需要先对其父类初始化
  • 当虚拟机启动时,用户指定的主类(含mian方法的类)会被虚拟机先初始化
  • 当使用JDK 1.7的动态语言支持时。如果一个java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,并这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化

这5种情况中的行为成为对一个类的主动引用,除此之外,其他引用被成为被动引用,不会触发初始化。

接口的加载过程与类加载过程稍微有些不同:一个接口在初始化时,并不要求其父类全部都完成了初始化,只有在真正使用到父接口的时候才会初始化。

类加载过程

加载

加载的过程

在类加载阶段,JVM做了三件事:

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

加载的来源

二进制字节流可以从多个地方读取:

  • 从zip包中读取
  • 从网络中读取(Applet)
  • 运行时计算生成(动态***)
  • 由其他文件生成(JSP)
  • 从数据库中读取,这种场景相对少见些

类和数组加载的不同

普通类的加载由类加载器完成。既可以使用系统提供的引导类加载器,也可以使用用户自定义的类加载器。
对于数组类而言,情况就有些不同了,数组类是由JVM直接创建的,一个数组类创建过程要遵循以下规则:

  • 如果数组的组件类型(Component Type,数组去掉一个维度的类型)时引用类型,则会加载这个组件类型,数组将在加载该组件类型的类加载器的类名称空间上被表示,即由JVM直接创建数组类,再由类加载器创建数组中的元素类
  • 如果数组的组件类型不是引用类型(int[]数组),JVM会将数组标记为与引导类加载器关联
  • 数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,则默认为public

加载过程的要点

  • JVM规范并未给出类在方法区中存放的数据结构:类完成加载后,二进制字节流就以特定的数据结构存储在方法区中,但存储的数据结构是由虚拟机自己定义的,JVM规范并没有指定。
  • JVM规范并没有指定Class对象存放的位置:在二进制字节流以特定格式存储在方法区后,JVM会创建一个java.lang.Class类型的对象,作为本类的外部接口。既然是对象就应该存放在堆内存中,不过JVM规范并没有给出限制,不同的虚拟机根据自己的需求存放这个对象。HotSpot将Class对象存放在方法区。
  • 加载阶段和连接阶段是交叉的

验证

验证阶段比较耗时,它非常重要但不一定必要,如果所运行的代码已经被反复使用和验证过,那么可以使用-Xverify:none参数关闭,以缩短类加载时间。

验证的目的

确保Class文件的字节流中包含的信息符合当前JVM的要求,且不会危害虚拟机自身安全

验证的必要性

Java语言是一门安全的语言,它能确保开发人员无法访问数组边界以外的内存、避免让一个对象转换成任意类型、避免跳转到不存在的代码行,如果出现这些情况,编译无法通过。也就是说,Java语言的安全性是通过编译器来保证的。
编译器和虚拟机是两个独立的东西,虚拟机只认二进制字节流,它不会管所获得的二进制字节流是哪来的,当然,如果是编译器给它的,那么就相对安全,但如果是从其它途径获得的,那么无法确保该二进制字节流是安全的。为了防止字节流中有安全问题,因此需要验证。

验证的过程

  1. 文件格式验证
    这个阶段主要验证输入的二进制字节流是否符合class文件结构的规范。二进制字节流只有通过了本阶段的验证,才会被允许存入到方法区中。本验证阶段是基于二进制字节流的,而后面的三个验证阶段都是在方法区中进行,并基于类特定的数据结构的。
  2. 元数据验证
    本阶段对方法区中的字节码描述信息进行语义分析,确保其符合Java语法规范。
  3. 字节码验证
    本阶段是验证过程的最复杂的一个阶段:对方法体进行语义分析,保证方法在运行时不会出现危害虚拟机的事件。
  4. 符号引用验证
    本阶段验证发生在解析阶段,确保解析能正常执行。

准备

准备的过程

准备阶段完成两件事情:

  1. 为已经在方法区中的类中的静态成员变量分配内存,类的静态成员变量也存储在方法区中。
  2. 为静态成员变量设置初始值,初始值为0、false、null等。

准备的注意点

被final修饰的常量如果有初始值,那么在编译阶段就会将初始值存入constantValue属性中,在准备阶段就将constantValue的值赋给该字段。

解析

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

初始化

初始化阶段就是执行类构造器clinit()的过程。
初始化的注意点:

  • clinit()方法由编译器自动产生,收集类中static{}代码块中的类变量赋值语句和类中静态成员变量的赋值语句。在准备阶段,类中静态成员变量已经完成了默认初始化,而在初始化阶段,clinit()方法对静态成员变量进行显示初始化
  • clinit()方法中静态成员变量的赋值顺序是根据Java代码中成员变量的出现的顺序决定的
  • 静态代码块能访问出现在静态代码块之前的静态成员变量,无法访问出现在静态代码块之后的成员变量
  • 静态代码块能给出现在静态代码块之后的静态成员变量赋值
  • 构造函数init()需要显示调用父类构造函数,而类的构造函数clinit()不需要调用父类的类构造函数,因为虚拟机会确保子类的clinit()方法执行前已经执行了父类的clinit()方法
  • 如果一个类/接口中没有静态代码块,也没有静态成员变量的赋值操作,那么编译器就不会生成clinit()方法
  • 接口也需要通过clinit()方法为接口中定义的静态成员变量显示初始化;接口中不能使用静态代码块;接口在执行clinit()方法前,虚拟机不会确保其父接口的clinit()方法被执行,只有当父接口中的静态成员变量被使用到时才会执行父接口的clinit()方法
  • 虚拟机会给clinit()方法加锁,因此当多条线程同时执行某一个类的clinit()方法时,只有一个方***被执行,其它的方法都被阻塞。并且,只要有一个clinit()方法执行完,其它的clinit()方法就不会再被执行。因此,在同一个类加载器下,同一个类只会被初始化一次