虚拟机类加载机制

虚拟机类加载机制

Scroll Down

在Java语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会在运行时因为类加载增加一些性能开销,但是也为Java应用程序带来了高度的灵活性,我们通过预定义和自定义的类加载器,可以让一个Java程序在运行时从网络或任何加载器中定义的方式加载一段二进制流作为程序代码的一部分

类在虚拟机中的生命周期

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载连接初始化使用卸载5个阶段,其中连接又可拆解为验证准备解析三个阶段

image.png

虚拟机对类必须进行初始化的时机给出了规定,那也就意味着加载验证准备必须要在其之前开始,解析并不一定需要在初始化之前开始,这是为了支持Java语言的运行时绑定,注意这里规定的是这些阶段开始的时机而非完成的时机,因为这些阶段通常都是互相交叉混合式进行的。

虚拟机必须立即初始化类的时机

虚拟机规范规定了,有且只有以下5种会触发类进行初始化的场景

  • 遇到new(新建)、getstatic(获取静态字段)、putstatic(静态字段赋值)或者invokestatic(调用静态方法)这4条字节码指令时
  • 使用java.lang.reflect包的方法对类进行反射调用的时候
  • 初始化一个类时,如果父类还没有进行过初始化,先触发父类初始化
  • 虚拟机启动时,用户指定的主类
  • 使用动态语言支持时,如果一个java.lang.invoke.MethodMandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄所对于应的类没有进行过初始化

类加载的过程

加载

类加载阶段,虚拟机规范规定了虚拟机必须完成以下三件事情

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

在上述规范中,由于只规定了通过一个类的全限定名来获取定义此类的二进制字节流,而未规定具体从哪里、怎样获取获取类的二进制流,这意味着我们可以从任何我们可以触达的地方,用任何可以实现的方式获取类,以下是几种常用的从Class文件外的地方加载类的加载方式:

  • 从压缩包中获取(Jar、War、Ear)
  • 从网络中获取(Applet)
  • 运行时计算生成(JDK Proxy、ASM)
  • 由其它文件生成(JSP)
  • 从数据库读取(SAP Netweaver)

开发人员可以通过系统提供的或者自定义的类加载器去自定义获取所有非数组类的二进制字节流(使用或重写类加载器的loadClass()方法)

数组类不通过类加载器创建,它是由Java虚拟机直接创建的,但数组的加载过程还是要用到数组中元素类型的类加载器,虚拟机规范中定义,一个数组类的创建过程遵循以下原则:

  • 如果数组的组件类型(指数组去掉一个维度的类型)是引用类型,就递归的使用类加载的过程去加载这个组件类型,该数组将在加载该组件类型的类加载器的类名称空间上被标识
  • 如果数组的组件类型不是引用类型,虚拟机会把数组标记为与引导类加载器关联
  • 数组类的可见性与它组件类型的可见性一致,如果组件类型不是引用类型,数组类的可见性会默认为public

验证

验证是连接阶段的第一步,目的是为了保证Class文件的字节流中包含的信息符合虚拟机要求且不会危害虚拟机的安全,验证阶段大致会完成以下4个阶段的校验动作

  • 文件格式验证:验证字节流是否符合Class文件格式规范、能否被当前版本虚拟机处理
  • 元数据验证:验证字节码描述信息(类结构定义)是否符合Java语言规范的要求
  • 字节码验证:验证字节码中语义是否合法,是否符合逻辑、是否会危害虚拟机(类方法体校验)
  • 符号引用验证:验证是否能通过符号引用在定义的可见性下找到对应的类、方法和字段

准备

准备阶段是正式为静态类变量分配内存,并设置初始值的阶段,基本类型赋值为默认值,被final static修饰的情况下会赋值为定义值,引用类型赋值为null,实例变量的赋值在初始化阶段完成

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用(虚拟机可以直接获取的内存地址或指针)的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行

初始化

初始化阶段是类加载过程中的最后一步,从初始化阶段开始,类中定义的Java程序代码才真正开始执行

在初始化阶段,虚拟机会收集类构造器中的<clinit>()方法并执行,<clinit>()方法由编译器自动收集类中的所有变量赋值操作和静态语句块中的语句合并而成,收集的顺序与Java源码中从上到下的顺序一致,子类初始化时会首先调用父类的<clinit>()方法,虚拟机实现时必须保证<clinit>()方法线程安全,保证每一个<clinit>()方法同一时间只有一个线程执行