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

  • 时间:
  • 浏览:2
  • 来源:幸运快3_快3倍率_幸运快3倍率

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

一、类加载机制概述

  亲戚亲戚大伙儿知道,一一有另八个 .java文件在编译全部都是形成相应的一一有另八个 或多个Class文件(若一一有另八个 类中涵盖结构类,则编译全部都是产生多个Class文件),但那些Class文件中描述的各种信息,最终都后会 加载到虚拟机中完后 后会 被运行和使用。事实上,虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可后会 能被虚拟机直接使用的Java类型的过程你这俩 你这俩 我虚拟机的 类加载机制。  

  与那些在编译时后会 进行连接工作的语言不同,在Java语言上边,类型的加载和连接全部都是在线程池池期间完成,从全部都是在类加载时稍微增加你这俩 性能开销,你这俩 却能为Java线程池池提供宽度的灵活性,Java中天生可后会 能动态扩展的语言形态多态你这俩 你这俩 我依赖运行期动态加载和动态链接你这俩 特点实现的。例如,你这俩 编写一一有另八个 使用接口的线程池池,可后会 能等到运行时再指定嘴笨 际的实现。你这俩 组装线程池池的土方式广泛应用于Java线程池池之中。

  既然从前,越来越 ,

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

本文主要对第一一有另八个 和第一有另八个 问题报告 进行阐述。


二. 类加载的时机 

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

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

  了解了Java类的生命周期完后 ,越来越 亲戚亲戚大伙儿现在来回答第一一有另八个 问题报告 :虚拟机那些完后 才会加载Class文件并初始化类呢?

1、类加载时机

  那些情形下虚拟机后会 结束了了了了加载一一有另八个 类呢?虚拟机规范中并越来越 对此进行强制约束,这点可后会 能交给虚拟机的具体实现来自由把握。

2、类初始化时机

  越来越 ,那些情形下虚拟机后会 结束了了了了初始化一一有另八个 类呢?这在虚拟机规范中是有严格规定的,虚拟机规范指明 有且后会 你这俩 情形后会 立即对类进行初始化(而你这俩 过程自然占据 在加载、验证、准备完后 ):

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

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

  2) 使用java.lang.reflect包的土方式对类进行反射调用的完后 ,你这俩 类越来越 进行过初始化,则后会 先触发其初始化。

  3) 当初始化一一有另八个 类的完后 ,你这俩 发现其父类还越来越 进行过初始化,则后会 先触发其父类的初始化。

  4) 当虚拟机启动时,用户后会 指定一一有另八个 要执行的主类(涵盖main()土方式的那个类),虚拟你这俩 先初始化你这俩 主类。

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

 注意,对于这你这俩 会触发类进行初始化的场景,虚拟机规范中使用了一一有另八个 很强烈的限定语:“有且后会 ”,这你这俩 场景中的行为称为对一一有另八个 类进行 主动引用。除此之外,所有引用类的土方式,全部都是会触发初始化,称为 被动引用。

  有点痛 后会 指出的是,类的实例化与类的初始化是一有另八个 全部不同的概念:

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

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类的符号引用入口,你这俩 有另八个 类在编译为Class文件完后 就不占据 关系了。


三. 类加载过程

  如上图所示,亲戚亲戚大伙儿在上文你这俩 提到过一一有另八个 类的生命周期包括加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 卸载(Unloading)七个阶段。现在亲戚亲戚大伙儿一一学习一下JVM在加载、验证、准备、解析和初始化八个阶段是如保对每个类进行操作的。

1、加载  

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

2、验证

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

3、准备

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

public static int v = 150150;

实际上变量 v 在准备阶段完后 的初始值为 0 而全部都是 150150, 将 v 赋值为 150150 的 put static 指令是线程池池被编译后, 存放于类构造器<client>土方式之中你这俩 注意你这俩 声明为 

public static final int v = 150150;

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

4、解析

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

  1. CONSTANT_Class_info

  2. CONSTANT_Field_info

  3. CONSTANT_Method_info等类型的常量。 

4.1 符号引用

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

 4.2 直接引用

   直接引用可后会 能是指向目标的指针,相对偏移量或是一一有另八个 能间接定位到目标的句柄。你这俩 有了直接引用,那引用的目标必定你这俩 在内存中占据 。 

5、初始化

  初始化阶段是类加载最后一一有另八个 阶段,前面的类加载阶段完后 ,除了在加载阶段可后会 能自定义类加载器以外,其它操作都由 JVM 主导。到了初始阶段,才结束了了了了真正执行类中定义的 Java 线程池池代码 。初始化阶段是执行类构造器<client>土方式的过程。 <client>土方式是由编译器自动分发类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟你这俩 保证子<client>土方式执行完后 ,父类的<client>土方式你这俩 执行完毕, 你这俩 一一有另八个 类中越来越 对静态变量赋值也越来越 静态语句块,越来越 编译器可后会 能不为你这俩 类生成<client>()土方式 

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

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

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

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

     发定义常量所在的类。

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

  5. 通过 Class.forName 加载指定类时,你这俩 指定参数 initialize 为 false 时,你这俩 你这俩 我会触发类初

   始化,嘴笨 你这俩 参数是告诉虚拟机,与否要对类进行初始化。

  6.
通过 ClassLoader 默认的 loadClass 土方式,你这俩 你这俩 我会触发初始化动作。

   虚拟你这俩 保证一一有另八个 类的类构造器<clinit>()在线程池池池环境中被正确的加锁、同步,你这俩 多个线程池池一齐去初始化一一有另八个 类,越来越 只会有一一有另八个 线程池池去执行你这俩 类的类构造器<clinit>(),你这俩 线程池池都后会 阻塞听候,直到活动线程池池执行<clinit>()土方式完毕。有点痛 后会 注意的是,在你这俩 情形下,你这俩 线程池池嘴笨 会被阻塞,但你这俩 执行<clinit>()土方式的那条线程池池退出后,你这俩 线程池池在唤醒完后 无需再次进入/执行<clinit>()土方式,你这俩 在同一一有另八个 类加载器下,一一有另八个 类型只会被初始化一次。你这俩 在一一有另八个 类的<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>() 时,你这俩 该土方式涵盖一一有另八个 死循环,你这俩 久久后会 退出。


四. 典型案例分析  

  在Java中, 创建一一有另八个 对象常常后会 经历如下多少过程:父类的类构造器<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对象,但你这俩 完后 类都越来越 初始化完毕啊,能直接进行实例化吗?事实上,这涉及到一一有另八个 根本问题报告 你这俩 你这俩 我:实例初始化不一定要在类初始化结束了了了完后 才结束了了了了初始化。 下面亲戚亲戚大伙儿结合类的加载过程说明你这俩 问题报告 。

  亲戚亲戚大伙儿知道,类的生命周期是:加载->验证->准备->解析->初始化->使用->卸载,你这俩 后会 在准备阶段和初始化阶段才会涉及类变量的初始化和赋值,你这俩 亲戚亲戚大伙儿只针对你这俩 有另八个 阶段进行分析:

  首先,在类的准备阶段后会 做的是为类变量(static变量)分配内存并设置默认值(零值),你这俩 在该阶段结束了了了后,类变量st将变为null、b变为0。有点痛 后会 注意的是,你这俩 类变量是final的,越来越 编译器在编译时就会为value生成ConstantValue属性,并在准备阶段虚拟机就会根据ConstantValue的设置将变量设置为指定的值。也你这俩 你这俩 我说,你这俩 上述程度对变量b采用如下定义土方式时:

 越来越 ,在准备阶段b的值你这俩 你这俩 我112,而不再是0了。

  此外,在类的初始化阶段后会 做的是执行类构造器<clinit>(),后会 指出的是,类构造器本质上是编译器分发所有静态语句块和类变量的赋值语句按语句在源码中的顺序合并生成类构造器<clinit>()。你这俩 ,对上述线程池池而言,JVM将先执行第一根绳子 静态变量的赋值语句:

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

  事实上,从Java宽度看,亲戚亲戚大伙儿知道一一有另八个 类初始化的基本常识,那你这俩 你这俩 我:在同一一有另八个 类加载器下,一一有另八个 类型只会被初始化一次。你这俩 你这俩 ,一旦结束了了了了初始化一一有另八个 类型,无论与否完成,后续全部都是会再重新触发该类型的初始化阶段了(只考虑在同一一有另八个 类加载器下的情形)。你这俩 ,在实例化上述线程池池中的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()调用父类构造土方式,在执行构造土方式完后 要先执行非静态代码块,你这俩 你这俩 顺序是 父类非静态代码块 》 父类构造函数 》 子类非静态代码块 》 子类构造函数

运行线程池池,就知道结果。只要真正理解类的实例化过程,例如问题报告 无需再难道亲戚亲戚大伙儿了!