JVM类生命周期概述:加载时机与加载过程

  • 时间:
  • 浏览:0
  • 来源:大发快3_快3豹子_大发快3豹子

  一5个 .java文件在编译总要形成相应的一5个 或多个Class文件,哪此Class文件中描述了类的各种信息,有但是它们最终都时要被加载到虚拟机中要能被运行和使用。事实上,虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可不时要被虚拟机直接使用的Java类型的过程假使 虚拟机的类加载机制。本文概述了JVM加载类的时机和心命周期,并结合典型案例重点介绍了类的初始化过程,进而了解JVM类加载机制。

一、类加载机制概述

  当当我们 知道,一5个 .java文件在编译总要形成相应的一5个 或多个Class文件(若一5个 类中中含内部管理类,则编译总要产生多个Class文件),但哪此Class文件中描述的各种信息,最终都时要加载到虚拟机中完后 要能被运行和使用。事实上,虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可不时要被虚拟机直接使用的Java类型的过程假使 虚拟机的 类加载机制。  

  与哪此在编译时时要进行连接工作的语言不同,在Java语言后面 ,类型的加载和连接完整版总要在系统系统进程运行期间完成,假使 会在类加载时稍微增加一些性能开销,有但是却能为Java应用系统系统进程提供角度的灵活性,Java中天生可不时要动态扩展的语言形状多态假使 依赖运行期动态加载和动态链接类式 特点实现的。类式 ,将会编写一5个 使用接口的应用系统系统进程,可不时要等到运行时再指定实在际的实现。类式 组装应用系统系统进程的方式 广泛应用于Java系统系统进程之中。

  既然假使 ,这样 ,

  • 虚拟机哪此完后 才会加载Class文件并初始化类呢?(类加载和初始化时机)
  • 虚拟机何如加载一5个 Class文件呢?(Java类加载的方式 :类加载器、双亲委派机制)
  • 虚拟机加载一5个 Class文件要经历哪此具体的步骤呢?(类加载过程/步骤)

本文主要对第一5个 和第一5个 现象进行阐述。


二. 类加载的时机 

  Java类从被加载到虚拟机内存中现在刚开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 卸载(Unloading)七个阶段。其中准备、验证、解析5个次责统称为连接(Linking),如图所示:

  加载、验证、准备、初始化和卸载这5个阶段的顺序是选着的,类的加载过程时要按照类式 顺序按部就班地现在刚开始,而解析阶段则不一定:它在一些情形下可不时要在初始化阶段完后 再现在刚开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。以下陈述的内容都已HotSpot为基准。有点儿时要注意的是,类的加载过程时要按照类式 顺序按部就班地“现在刚开始”,而完整版总要按部就班的“进行”或“完成”,将会哪此阶段通常完整版总要相互交叉地混合式进行的,也假使 说通常会在一5个 阶段执行的过程中调用或激活另外一5个 阶段。

  了解了Java类的生命周期完后 ,这样 当当我们 现在来回答第一5个 现象:虚拟机哪此完后 才会加载Class文件并初始化类呢?

1、类加载时机

  哪此情形下虚拟机时要现在刚开始加载一5个 类呢?虚拟机规范中并这样 对此进行强制约束,这点可不时要交给虚拟机的具体实现来自由把握。

2、类初始化时机

  这样 ,哪此情形下虚拟机时要现在刚开始初始化一5个 类呢?这在虚拟机规范中是有严格规定的,虚拟机规范指明 有且必须 一种生活情形时要立即对类进行初始化(而类式 过程自然发生在加载、验证、准备完后 ):

  1) 遇到new、getstatic、putstatic或invokestatic这四条字节码指令(注意,newarray指令触发的假使 数组类型一种生活的初始化,而无需因为其相关类型的初始化,比如,new String[]只会直接触发String[]类的初始化,也假使 触发对类[Ljava.lang.String的初始化,而直接无需触发String类的初始化)时,将会类这样 进行过初始化,则时要先对其进行初始化。生成这四条指令的最常见的Java代码场景是:

  • 使用new关键字实例化对象的完后 ;
  • 读取或设置一5个 类的静态字段(被final修饰,已在编译器把结果放进常量池的静态字段除外)的完后 ;
  • 调用一5个 类的静态方式 的完后 。

  2) 使用java.lang.reflect包的方式 对类进行反射调用的完后 ,将会类这样 进行过初始化,则时要先触发其初始化。

  3) 当初始化一5个 类的完后 ,将会发现其父类还这样 进行过初始化,则时要先触发其父类的初始化。

  4) 当虚拟机启动时,用户时要指定一5个 要执行的主类(中含main()方式 的那个类),虚拟将会先初始化类式 主类。

  5) 当使用jdk1.7动态语言支持时,将会一5个 java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方式 句柄,有但是类式 方式 句柄所对应的类这样 进行初始化,则时要先出触发其初始化。

 注意,对于类式 种生活会触发类进行初始化的场景,虚拟机规范中使用了一5个 很强烈的限定语:“有且必须”,类式 种生活场景中的行为称为对一5个 类进行 主动引用。除此之外,所有引用类的方式 ,完整版总要会触发初始化,称为 被动引用。

  有点儿时要指出的是,类的实例化与类的初始化是一5个 完整版不同的概念:

  • 类的实例化是指创建一5个 类的实例(对象)的过程;
  • 类的初始化是指为类中各个类成员(被static修饰的成员变量)赋初始值的过程,是类生命周期中的一5个 阶段。

3、被动引用的几种经典场景

  1)、通过子类引用父类的静态字段,无需因为子类初始化

public class SSClass{
    static{
        System.out.println("SSClass");
    }
}  

public class SClass extends SSClass{
    static{
        System.out.println("SClass init!");
    }

    public static int value = 123;

    public SClass(){
        System.out.println("init SClass");
    }
}

public class SubClass extends SClass{
    static{
        System.out.println("SubClass init");
    }

    static int a;

    public SubClass(){
        System.out.println("init SubClass");
    }
}

public class NotInitialization{
    public static void main(String[] args){
        System.out.println(SubClass.value);
    }
}
/* Output: 
        SSClass
        SClass init!
        123     
 */

 对于静态字段,必须直接定义类式 字段的类才会被初始化,有但是通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而无需触发子类的初始化。在本例中,将会value字段是在类SClass中定义的,有但是该类会被初始化;此外,在初始化类SClass时,虚拟将会发现其父类SSClass还未被初始化,有但是虚拟机将先初始化父类SSClass,有但是初始化子类SClass,而SubClass始终无需被初始化。

 2)、通过数组定义来引用类,无需触发此类的初始化

public class NotInitialization{
    public static void main(String[] args){
        SClass[] sca = new SClass[10];
    }
}

3)、常量在编译阶段会存入调用类的常量池中,本质上并这样 直接引用到定义常量的类,有但是无需触发定义常量的类的初始化

public class ConstClass{

    static{
        System.out.println("ConstClass init!");
    }

    public static  final String CONSTANT = "hello world";
}

public class NotInitialization{
    public static void main(String[] args){
        System.out.println(ConstClass.CONSTANT);
    }
}
/* Output: 
        hello world
 */

上述代码运行完后 ,只输出 “hello world”,这是将会实在在Java源码中引用了ConstClass类中的常量CONSTANT,有但是编译阶段将此常量的值“hello world”存储到了NotInitialization常量池中,对常量ConstClass.CONSTANT的引用实际都被转化为NotInitialization类对自身常量池的引用了。也假使 说,实际上NotInitialization的Class文件之中并这样 ConstClass类的符号引用入口,类式 5个 类在编译为Class文件完后 就不发生关系了。


三. 类加载过程

  如上图所示,当当我们 在上文将会提到过一5个 类的生命周期包括加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 卸载(Unloading)七个阶段。现在当当我们 一一学习一下JVM在加载、验证、准备、解析和初始化5个阶段是何如对每个类进行操作的。

1、加载  

  加载是类加载过程中的一5个 阶段, 类式 阶段会在内存中生成一5个 代表类式 类的 java.lang.Class 对作为方式 区类式 类的各种数据的入口。注意这里不一定非得要从一5个 Class 文件获取,这里既可不时要从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可不时要在运行时计算生成(动态代理),也可不时要由其它文件生成(比如将 JSP 文件转添加对应的 Class 类)。 

2、验证

  类式 阶段的主要目的是为了确保 Class 文件的字节流中中含的信息否是是符合当前虚拟机的要求,并且无需危害虚拟机自身的安全。

3、准备

  准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方式 区中分配哪此变量所使用的内存空间。注意这里所说的初始值概念,比如一5个 类变量定义为 

public static int v = 60

60

;

实际上变量 v 在准备阶段完后 的初始值为 0 而完整版总要 60 60 , 将 v 赋值为 60 60 的 put static 指令是系统系统进程被编译后, 存放于类构造器<client>方式 之中有但是注意将会声明为 

public static final int v = 60

60

;

在编译阶段会为 v 生成 ConstantValue 属性,在准备阶段虚拟将会根据 ConstantValue 属性将 v赋值为 60 60 。 

4、解析

解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用假使 class 文件中的:

  1. CONSTANT_Class_info

  2. CONSTANT_Field_info

  3. CONSTANT_Method_info等类型的常量。 

4.1 符号引用

   符号引用与虚拟机实现的布局无关, 引用的目标无需说一定要将会加载到内存中各种虚拟机实现的内存布局可不时要各不相同,有但是它们能接受的符号引用时只是我一致的,将会符号引用的字面量形式明选着义在 Java 虚拟机规范的 Class 文件格式中 

 4.2 直接引用

   直接引用可不时要是指向目标的指针,相对偏移量或是一5个 能间接定位到目标的句柄。将会有了直接引用,那引用的目标必定将会在内存中发生。 

5、初始化

  初始化阶段是类加载最后一5个 阶段,前面的类加载阶段完后 ,除了在加载阶段可不时要自定义类加载器以外,其它操作都由 JVM 主导。到了初始阶段,才现在刚开始真正执行类中定义的 Java 系统系统进程代码 。初始化阶段是执行类构造器<client>方式 的过程。 <client>方式 是由编译器自动挂接类中的类变量的赋值操作和静态得话块中的得话合并而成的。虚拟将会保证子<client>方式 执行完后 ,父类的<client>方式 将会执行完毕, 将会一5个 类中这样 对静态变量赋值也这样 静态得话块,这样 编译器可不时要不为类式 类生成<client>()方式  

 注意以下几种情形无需执行类初始化:

  1. 通过子类引用父类的静态字段,只会触发父类的初始化,而无需触发子类的初始化。

  2. 定义对象数组,无需触发该类的初始化。

  3. 常量在编译期间会存入调用类的常量池中,本质上并这样 直接引用定义常量的类,无需触

     发定义常量所在的类。

  4. 通过类名获取 Class 对象,无需触发类的初始化。

  5. 通过 Class.forName 加载指定类时,将会指定参数 initialize 为 false 时,假使 会触发类初

   始化,实在类式 参数是告诉虚拟机,否是是要对类进行初始化。

  6.
通过 ClassLoader 默认的 loadClass 方式 ,假使 会触发初始化动作。

   虚拟将会保证一5个 类的类构造器<clinit>()在多系统系统进程环境中被正确的加锁、同步,将会多个系统系统进程一起去去初始化一5个 类,这样 只会有一5个 系统系统进程去执行类式 类的类构造器<clinit>(),一些系统系统进程都时要阻塞听候,直到活动系统系统进程执行<clinit>()方式 完毕。有点儿时要注意的是,在类式 情形下,一些系统系统进程实在会被阻塞,但将会执行<clinit>()方式 的那条系统系统进程退出后,一些系统系统进程在唤醒完后 无需再次进入/执行<clinit>()方式 ,将会 在同一5个 类加载器下,一5个 类型只会被初始化一次。将会在一5个 类的<clinit>()方式 中含耗时很长的操作,就将会造成多个系统系统进程阻塞,在实际应用中类式 阻塞往往是隐藏的,如下所示:

public class DealLoopTest {
    static{
        System.out.println("DealLoopTest...");
    }
    static class DeadLoopClass {
        static {
            if (true) {
                System.out.println(Thread.currentThread()
                        + "init DeadLoopClass");
                while (true) {      // 模拟耗时很长的操作
                }
            }
        }
    }

    public static void main(String[] args) {
        Runnable script = new Runnable() {   // 匿名内部管理类
            public void run() {
                System.out.println(Thread.currentThread() + " start");
                DeadLoopClass dlc = new DeadLoopClass();
                System.out.println(Thread.currentThread() + " run over");
            }
        };

        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
    }
}
/* Output: 
        DealLoopTest...
        Thread[Thread-1,5,main] start
        Thread[Thread-0,5,main] start
        Thread[Thread-1,5,main]init DeadLoopClass
 */

如上述代码所示,在初始化DeadLoopClass类时,系统系统进程Thread-1得到执行并在执行类式 类的类构造器<clinit>() 时,将会该方式 中含一5个 死循环,有但是久久必须退出。


四. 典型案例分析  

  在Java中, 创建一5个 对象常常时要经历如下哪几个过程:父类的类构造器<clinit>() -> 子类的类构造器<clinit>() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数。

这样 ,当当我们 看看下面的系统系统进程的输出结果:

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;     // 静态变量
}
/* Output: 
        2
        3
        a=110,b=0
        1
        4
 */

当当我们 能得到正确答案吗?实在笔者勉强猜出了正确答案,但总感觉有点儿。将会在初始化阶段,当JVM对类StaticTest进行初始化时,首先会执行下面的得话:

static StaticTest st = new StaticTest();

也假使 实例化StaticTest对象,但类式 完后 类都这样 初始化完毕啊,能直接进行实例化吗?事实上,这涉及到一5个 根本现象假使 :实例初始化不一定要在类初始化现在刚开始完后 才现在刚开始初始化。 下面当当我们 结合类的加载过程说明类式 现象。

  当当我们 知道,类的生命周期是:加载->验证->准备->解析->初始化->使用->卸载,有但是必须在准备阶段和初始化阶段才会涉及类变量的初始化和赋值,你要们只针对类式 5个 阶段进行分析:

  首先,在类的准备阶段时要做的是为类变量(static变量)分配内存并设置默认值(零值),有但是在该阶段现在刚开始后,类变量st将变为null、b变为0。有点儿时要注意的是,将会类变量是final的,这样 编译器在编译时就会为value生成ConstantValue属性,并在准备阶段虚拟机就会根据ConstantValue的设置将变量设置为指定的值。也假使 说,将会上述程度对变量b采用如下定义方式 时:

 这样 ,在准备阶段b的值假使 112,而不再是0了。

  此外,在类的初始化阶段时要做的是执行类构造器<clinit>(),时要指出的是,类构造器本质上是编译器挂接所有静态得话块和类变量的赋值得话按得话在源码中的顺序合并生成类构造器<clinit>()。有但是,对上述系统系统进程而言,JVM将先执行第第一条静态变量的赋值得话:

  在类都这样 初始化完毕完后 ,能直接进行实例化相应的对象吗?

  事实上,从Java角度看,当当我们 知道一5个 类初始化的基本常识,那假使 :在同一5个 类加载器下,一5个 类型只会被初始化一次。全都,一旦现在刚开始初始化一5个 类型,无论否是是完成,后续完整版总要会再重新触发该类型的初始化阶段了(只考虑在同一5个 类加载器下的情形)。有但是,在实例化上述系统系统进程中的st变量时,实际上是把实例初始化嵌入到了静态初始化流程中,有但是在后面 的系统系统进程中,嵌入到了静态初始化的起始位置。这就因为了实例初始化完整版发生在静态初始化完后 ,当然,这也是因为a为110b为0的因为。

  有但是,上述系统系统进程的StaticTest类构造器<clinit>()的实现等价于:

public class StaticTest {
    <clinit>(){
        a = 110;    // 实例变量
        System.out.println("2");        // 实例代码块
        System.out.println("3");     // 实例构造器中代码的执行
        System.out.println("a=" + a + ",b=" + b);  // 实例构造器中代码的执行
        类变量st被初始化
        System.out.println("1");        //静态代码块
        类变量b被初始化为112
    }
}

有但是,上述系统系统进程会有后面 的输出结果。下面,当当我们 对上述系统系统进程稍作改动,在系统系统进程最后的一行,增加以下代码行:

 static StaticTest st1 = new StaticTest();

这样 ,此时系统系统进程的输出又是哪此呢?将会你对上述的内容理解很好得话,这样 得出结论(必须执行完上述代码行后,StaticTest类才被初始化完成),即:

2
3
a=110,b=0
1
2
3
a=110,b=112
4

这样 下面的系统系统进程的执行结果是哪此呢???

class Foo {
    int i = 1;

    Foo() {
        System.out.println(i);             
        int x = getValue();
        System.out.println(x);            
    }

    {
        i = 2;
    }

    protected int getValue() {
        return i;
    }
}

//子类
class Bar extends Foo {
    int j = 1;

    Bar() {
        j = 2;
    }

    {
        j = 3;
    }

    @Override
    protected int getValue() {
        return j;
    }
}

public class ConstructorExample {
    public static void main(String... args) {
        Bar bar = new Bar();
        System.out.println(bar.getValue());        
    }
}

在创建对象前,先进行类的初始化,类的初始化会将所有非静态代码块挂接起来先执行,而父类时要先于子类初始化,全都父类静态代码块先执行,接着是子类静态代码块。此时类初始化完成。接下来要创建子类实例,子类通过super()调用父类构造方式 ,在执行构造方式 完后 要先执行非静态代码块,全都顺序是 父类非静态代码块 》 父类构造函数 》 子类非静态代码块 》 子类构造函数

运行系统系统进程,就知道结果。假使 真正理解类的实例化过程,类式 现象无需再难道当当我们 了!