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

  • 时间:
  • 浏览:0

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

一、类加载机制概述

  .我知道,一十个 .java文件在编译一定会形成相应的一十个 或多个Class文件(若一十个 类含有高内内外部类,则编译一定会产生多个Class文件),但这俩 Class文件中描述的各种信息,最终都需用加载到虚拟机中之后也能被运行和使用。事实上,虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成还需用被虚拟机直接使用的Java类型的过程只是虚拟机的 类加载机制。  

  与这俩 在编译需用用进行连接工作的语言不同,在Java语言中间,类型的加载和连接一定会在tcp连接运行运行期间完成,好难 会在类加载时稍微增加其他性能开销,因此却能为Javatcp连接运行运行提供角度的灵活性,Java中天生还需用动态扩展的语言社会形态多态只是依赖运行期动态加载和动态链接这俩 特点实现的。类事,原应 编写一十个 使用接口的tcp连接运行运行,还需用等到运行时再指定实在际的实现。这俩 组装tcp连接运行运行的土方法广泛应用于Javatcp连接运行之中。

  既然好难 ,好难,

  • 虚拟机这俩 之后才会加载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 = 60

60

;

实际上变量 v 在准备阶段之后的初始值为 0 而一定会 60 60 , 将 v 赋值为 60 60 的 put static 指令是tcp连接运行被编译后, 存放于类构造器<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、初始化

  初始化阶段是类加载最后一十个 阶段,前面的类加载阶段之后,除了在加载阶段还需用自定义类加载器以外,其它操作都由 JVM 主导。到了初始阶段,才结速真正执行类中定义的 Java tcp连接运行代码 。初始化阶段是执行类构造器<client>土方法的过程。 <client>土方法是由编译器自动挂接类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟原应 保证子<client>土方法执行之后,父类的<client>土方法原应 执行完毕, 原应 一十个 类中好难对静态变量赋值也好难静态语句块,好难编译器还需用不为这俩 类生成<client>()土方法 

 注意以下几种情况汇报不需要执行类初始化:

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

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

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

     发定义常量所在的类。

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

  5. 通过 Class.forName 加载指定类时,原应 指定参数 initialize 为 false 时,只是会触发类初

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

  6.
通过 ClassLoader 默认的 loadClass 土方法,只是会触发初始化动作。

   虚拟原应 保证一十个 类的类构造器<clinit>()在多tcp连接运行环境中被正确的加锁、同步,原应 多个tcp连接运行共共同初始化一十个 类,好难只会一十个 tcp连接运行去执行这俩 类的类构造器<clinit>(),其他tcp连接运行都需用阻塞守候,直到活动tcp连接运行执行<clinit>()土方法完毕。怪怪的需用注意的是,在这俩 情况汇报下,其他tcp连接运行实在会被阻塞,但原应 执行<clinit>()土方法的那条tcp连接运行退出后,其他tcp连接运行在唤醒之后不需要再次进入/执行<clinit>()土方法,原应 在同一十个 类加载器下,一十个 类型只会被初始化一次。原应 在一十个 类的<clinit>()土方法含有耗时很长的操作,就原应 造成多个tcp连接运行阻塞,在实际应用中这俩 阻塞往往是隐藏的,如下所示:

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类时,tcp连接运行Thread-1得到执行并在执行这俩 类的类构造器<clinit>() 时,原应 该土方法含有一十个 死循环,因此久久也能退出。


四. 典型案例分析  

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

好难,.我看看下面的tcp连接运行的输出结果:

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>()。因此,对上述tcp连接运行而言,JVM将先执行第第一根静态变量的赋值语句:

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

  事实上,从Java角度看,.我知道一十个 类初始化的基本常识,那只是:在同一十个 类加载器下,一十个 类型只会被初始化一次。全都,一旦结速初始化一十个 类型,无论是否完成,后续一定会会再重新触发该类型的初始化阶段了(只考虑在同一十个 类加载器下的情况汇报)。因此,在实例化上述tcp连接运行中的st变量时,实际上是把实例初始化嵌入到了静态初始化流程中,因此在中间的tcp连接运行中,嵌入到了静态初始化的起始位置。这就原应 了实例初始化全版发生在静态初始化之后,当然,这也是原应 a为110b为0的原应 。

  因此,上述tcp连接运行的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
    }
}

因此,上述tcp连接运行会有中间的输出结果。下面,.我对上述tcp连接运行稍作改动,在tcp连接运行最后的一行,增加以下代码行:

 static StaticTest st1 = new StaticTest();

好难,此时tcp连接运行的输出又是这俩 呢?原应 你对上述的内容理解很好语句,好难得出结论(也能执行完上述代码行后,StaticTest类才被初始化完成),即:

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

好难下面的tcp连接运行的执行结果是这俩 呢???

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()调用父类构造土方法,在执行构造土方法之后要先执行非静态代码块,全都顺序是 父类非静态代码块 》 父类构造函数 》 子类非静态代码块 》 子类构造函数

运行tcp连接运行,就知道结果。只要真正理解类的实例化过程,类事大大问题 不需要再难道.我了!