vlambda博客
学习文章列表

JVM类:加载时间与加载过程

一、类加载概述

类的整个生命周期包含七个阶段:加载、验证、准备、解析、初始化、使用、卸载七个阶段。其中类加载的全过程包含五个阶段:加载、验证、准备、解析、初始化。其中解析阶段可以在初始化前也可以在初始化后,在初始化后是为了支持java语言的动态绑定。其他6个阶段是严格顺序的,不能颠倒。


二、类初始化时机

1、遇到new,getstatic,putstatic,invokestatic四条指令时,如下三个场景下:

  • 使用new关键字实例化对象时

  • 读取或者设置一个类的静态字段(final修饰的静态字段除外)

  • 调用一个类的静态方法。

2、使用java.lang.reflect包的方法对类进行反射调用时,如果类没有被初始化,需要先初始化

3、调用一个类时,如果其父类没有初始化,则需要先初始化其父类

4、当虚拟机启动时,用户需要指定一个执行的主类,虚拟机会先初始化这个类;

5、当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandler实例的最后解析结果是REF_getstatic,REF_putstatic,REF_invokestatic的方法句柄,如果这个方法句柄对应的类没有被初始化,则需要初始化。

这五种场景被称为对一个类进行主动引用。除此之外所有引用类的方式,都是对类的被动引用。


三、类的被动引用

1、通过子类引用父类的静态变量,不会导致子类的初始化

对于静态字段,只用直接定义这个字段的类及其父类会被初始化,子类不会被初始化。

2、通过数组定义来引用类,不会触发引用类的初始化

只会初始化一个继承Object的数组对象。

3、引用static final的常量,不会触发类的初始化

static final修饰的常量,在编译阶段已经存储到常量池中,引用是不涉及类的初始化。


四、类加载全过程

1、加载

在加载阶段,虚拟机需要完成三件事情:

  • 通过一个类的全限定名获取定义此类的二进制字节流

  • 经这个字节流所代表的静态存储结构转化为方法区运行的数据结构;

  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。


2、验证

验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  • 文件格式验证:验证字节流是否符合class文件格式的规范,并且能够被当前版本的虚拟机处理(是否以魔数0xCAFEBABE开头,主、次版本号是否在当前虚拟机的处理范围,常量池中的常量是否有不被支持的常量类型等),保证输入的字节流能正确地解析并存储到方法区之内,格式符合描述一个java类型信息的要求。

  • 元数据验证:对字节码描述的信息进行语义解析,以保证其描述的信息符合java语言规范的要求(类是否有父类,类的父类是否继承了不允许继承的final修饰类,类是不是抽象类,是否实现类父类或者接口中的所有方法,类中字段或者方法是否与父类冲突,比如覆盖了父类的final方法,或者不符合重载规则等),即对元数据信息进行语义校验,保证不存在不符合java语言规范的元数据信息;

  • 字节码验证:是验证中最复杂的阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,保证被验证类的方法在运行时不会做出危害虚拟机的事件(保证任何时候操作数栈的数据类型与指令代码序列都能配合工作,例如在操作数栈放了int类型的数据,使用时却按long类型进行使用;保证跳转指令不会跳转到方法体以外的字节码指令上;保证方法体中的类型转换是安全的等)

  • 符合引用验证:发生在虚拟机将符合引用转化为直接引用的时候,这个将会在解析阶段发生,验证对类自身以外的信息进行匹配性校验(符合引用中的类、方法、字段的访问性是否可以被当前类访问等)

对虚拟机的类加载机制来说,验证阶段非常重要,但不是必须。如果所运行的全部代码都已经被反复使用和验证过,这个阶段可使用-Xverify:none参数来关闭大部分验证,从而节省虚拟机类加载的时间。


3、准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。注意:这个时候进行的内存分配仅包含类变量(被static修饰的变量),而不包含实例变量,实例变量将在对象实例化时随对象一起分配在java的堆中;其次,这里说的初始值“通常情况”下是数据累的的零值。

public static int value = 123;//其初始值为0

“特殊情况”,如果类字段属性表中存在ConstantValue属性,那么准备阶段变量value就会被初始化为ConstantValue所指定的值。

public static final int value = 123;//其初始值w诶123


4、解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,主要针对对或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限制7类符号引用进行解析。


5、初始化

类初始化阶段是类加载的最后一个阶段,前面的类加载过程中,除了加载阶段用户应用程序可以通过自定义加载器参与外,其他的动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的java程序代码。


  • 初始化阶段是执行类构造器<cinit>方法的过程。<cinit>方法时由编译器自动收集类的所有类变量和赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是有语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但不可以访问。

public class Test { static {        i = 0;  //可以赋值        System.out.println(i); //不可以访问,非法引用 }  static int i = 1;}
  • <cinit>方法和类的构造函数不同,它不需要显示的调用父类的构造器,虚拟机会保证在子类的<cinit>方法执行之前,执行父类的<cinit>方法。因此虚拟机中第一个执行的<cinit>方法可定是java.lang.Object的。

  • 由于父类的<cinit>方法先执行,也就意味着父类中定义的静态语句块优先于子类的变量赋值操作。

static class Parent { public static int A = 1; static { A = 2 }}
static class Sub extends Parent { public static int B = A}
public static void main(String[] args) {    System.out.println(Sub.B);    //输出值为2}
  • <cinit>方法对于类或接口来说并不是必须的,如果一个类中没有静态的语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<cinit>方法。

  • 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<cinit>方法。但接口与类不同,执行接口的<cinit>方法时不需要先执行父类的<cinit>。只有当父类中定义的变量使用时,父类才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<cinit>方法。

  • 虚拟机会保证一个类的<cinit>方法在多线程环境下被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<cinit>方法,其他线程都阻塞等待,直到活动线程执行<cinit>方法完毕。如果一个类的<cinit>方法比较耗时,就可能造成多个线程的阻塞,在时间应用中,这种阻塞往往很隐蔽。

package com.eddie;
public class StaticTest { public static void main(String[] args) { staticFunction(); }
static StaticTest st = new StaticTest(); static { System.out.println("1"); } { System.out.println("2"); } StaticTest() { System.out.println("3"); System.out.println("a=" + a +",b=" + b); }
public static void staticFunction() { System.out.println("4"); }
int a = 110; static int b = 112;}
//运行结果23a=110,b=014