虚拟机七:类加载机制(一)之类加载过程

虚拟机真正加载的是.class的二进制流,所以并不要求.class的来源必须是本地磁盘,也可以是网络或其他地方,这当然取决于使用怎样的类加载器。从简单的Applet、JSP到相对复杂的OSGI技术都依赖这样的特性。

类的生命周期

虚拟机七:类加载机制(一)之类加载过程

其中,加载、校验、准备与初始化是必要的步骤。步骤执行并非严格的顺序执行,是在执行一个阶段的同时也可能开始了后续的阶段。

类加载的过程

加载:

虚拟机需要进行①加载一个二进制流②方法区保存此二进制流转化后的静态存储结构③内存中生成一个Class对象作为方法区访问对应数据的入口,该对象也保存在方法区中

校验:

校验的工作其实发生在加载时,以确保文件时jvm支持的格式,并且不会对虚拟机产生危害。比如Magic Number如果不是0xCAFEBABY就没必要再执行后续的动作了,抛出java.lang.VerifyError异常或子异常。

验证大致分为四个阶段:

  • 文件格式校验:比如魔数、主次版本号等。。。
  • 元数据校验:类的语义校验。类的继承关系、属性是否与父类冲突等。。。
  • 字节码校验:字节码语义校验。类型检查,1.6新增的StackMapTable属性保存本地变量表与操作栈应有的对照关系,避免了虚拟机的类型推导工作
  • 符号引用检验:其实发生在解析阶段,将符号引用转为直接引用。检查类、字段和方法的访问限制等,会抛出java.lang.NoSuchMethodError、java.lang.NoSuchFieldError等异常。

检验步骤并非时JVM类加载时必须的,在能确认所有代码安全的情况下,使用-Xverify:none关闭校验提升类加载的速度。

准备:

在方法区上为类变量(被static修饰)分配内存并设置初始值,实例变量跟随实例对象一同在堆上分配内存。

类变量一开始都设置一个零值。之后的赋值指令是在类构造方法<clinit>()时执行(此步骤发生在初始化阶段),如果类变量又被final修饰,则类变量的属性值将从ConstantValue中获取。

解析:

虚拟机将常量池中的符号引用转变为直接引用的过程。解析的过程中也伴随着检验操作

解析行为在第一次解析之后会缓存解析结果,避免重复解析。invokedynamic指令除外,因为此指令专门用于动态语言支持的。其实虚拟机对invokedynamic指令不做解析不会生成字节码指令,执行的时候才进行解析。

解析的七类对象:类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符(先讲前4种,关乎动态语言支持的后3种后面说):

  • 类或接口的解析:将字符引用的字面量替换成直接引用,如果有继承关系就需要递归执行。数组和对象处理方式不一样
  • 字段的解析:如果该类本身就包含了简单名称和子段描述符与要求相符合,就直接返回此字段的引用,查找结束,否则按照继承关系查找符合的字段。返回了字段的引用还会校验字段的权限
  • 类方法的解析:与字段解析步骤差不多,先检查本类中是否有符合要求的方法的简单名称与描述符,没有的话再按照继承关系递归查找,找到之后返回其直接引用,还需校验方法的访问权限。前提要校验该类不能是接口
  • 接口方法的解析:与类方法解析相同。只是不需要检验方法的访问权限

初始化:

类加载的最后一步。前面的步骤中,用户只可以控制加载阶段,其他阶段都由虚拟机主导。

初始化阶段才真正的执行类中定义的Java代码(或者说字节码)。

准备阶段,虚拟机已经对变量进行了一次零值设置。初始化阶段则按照程序员的主观意识进行属性设置。从虚拟机的角度讲:初始化阶段将调用类构造器<clinit>()方法,这是虚拟机自动生成的方法。

<clinit>()细节:

  • 主要工作是为类变量赋值、执行静态代码块
  • 与实例的构造函数<init>不同,虚拟机会确保父类的类构造器先被执行,第一个就是Object的<clinit>()
  • 父类的类变量赋值操作肯定早于子类
  • 如果类没有静态代码块也没有类变量的赋值,就不会产生<clinit>()
  • 接口中没有static{},但也有类变量赋值,也会产生<clinit>(),只是不要求父接口的<clinit>()先执行。同样接口的实现类的<clinit>()执行前也不会要求接口的<clinit>()先被执行;只有调用接口的变量时才会执行
  • <clinit>()会面临多线程执行,虚拟机通过加锁来避免类变量和静态代码块被多次执行。但这种阻塞是不可见的,你可以在静态代码块中写个死循环试试