类加载过程

以前在网上看到一道面试题,很形象的描述了类的加载初始化过程。要彻底理解这道题,就不得不深刻理解类加载的过程。面试题以下:html

class SingleTon {
    private static SingleTon singleTon = new SingleTon();
    public static int count1;
    public static int count2 = 0;
 
    private SingleTon() {
        count1++;
        count2++;
    }
 
    public static SingleTon getInstance() {
        return singleTon;
    }
}
 
public class Test {
    public static void main(String[] args) {
        SingleTon singleTon = SingleTon.getInstance();
        System.out.println("count1=" + singleTon.count1);
        System.out.println("count2=" + singleTon.count2);
    }
}

这道题的正确答案为 :java

count1=1面试

count2=0数组

至于为何会是这个答案,这就涉及到了 JVM 类加载的过程。安全

类的生命周期

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括加载、验证、准备、解析、初始化、使用和卸载 7 个阶段,其中验证、准备和解析 3 个阶段统称为链接,这 7 个阶段发生的顺序以下图所示。网络

类的生命周期

加载、验证、准备、初始化和卸载这 5 个阶段的顺序是肯定的。解析阶段则不必定,因为支持运行时绑定,类能够在初始化以后再开始进行解析。同时这些阶段只是按照顺序进行开始,并不必定会按照顺序进行或者结束,由于这些阶段一般都是互相交叉地混合式进行的,一般会在一个阶段执行过程当中调用、激活另一个阶段。数据结构

类的加载过程

加载

加载是类加载过程的一个阶段,是根据特定名称查找类或接口类型的二进制表示(binary representation),并由此二进制表示来建立类或接口的过程。在加载阶段,虚拟机须要完成 3 件事:多线程

  1. 经过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个表明这个类的 java.lang.Class 对象,做为方法区这个类的各类数据的访问入口。

加载途径

虚拟机加载的是类的二进制流,只是对内容格式作了限制,并无指名要从哪里去获取、怎样获取一个类的二进制流,比较常见的有一下几种:oracle

  • 从 jar、ear、war 包中读取。
  • 从网络流中读取,这种场景的典型应用就是 Applet。
  • 运行时计算生成,使用最多的就是动态代理技术。
  • 其余文件生成,典型的场景是 JSP 应用,即由 JSP 文件生成对应的 Class 类。

加载方式

  • 对于一个非数组类,加载阶段可使用系统提供的引导类加载器来完成,也能够由用户自定义的类加载器去完成。
  • 数组类是 Java 虚拟机直接建立的。一个数组类建立的过程遵循如下规则:
    • 数组的元素是引用类型,那就递归采用本文所讲的类加载过程加载这个元素,数组类将在加载该元素类型的类加载器的类名称空间上被标识。
    • 若是数组的元素类型不是引用类型(例如:int[] 数组),Java 虚拟机将会把数组类标记与引导类加载器关联。
    • 数组的可见性与他的元素类型的可见性一致,若是元素类型不是引用类型,那数组的可见性将默认为 public。

加载阶段完成后,二级制字节流就按照虚拟机所需的格式储存在方法区之中,而后在内存中实例化一个 java.lang.Class 对象,将这个对象做为程序访问方法区中的这些类型数据的外部接口。jvm

验证

验证时链接阶段的第一步,这一步是为了保证 Class文件二进制字节流符合虚拟机规范,而且不会危害虚拟机自身的安全。Java 虚拟机规范有大量的约束和验证规则,详细的描述的验证过程。验证过程主要仍是围绕 Class 文件格式对各部分进行验证。Class 文件格式课参考另外一篇博文字节码文件结构详解。但从总体上看,验证阶段大体会完成下面 4 个阶段的验证动做。

文件格式验证

第一阶段要验证字节流是否符合 Class文件格式规范,而且能被当前版本的虚拟机处理。这一阶段可能包括下面验证点:

  • 是否一魔数 0xCAFEBEBE 开头。
  • 主次版本号是否在当前虚拟机处理范围以内。
  • 常量池中的常量是否有不被支持的常量类型(检查常量 tag 标志)。
  • 指向常量的各类索引值中是否有指向不存在的常量或不符合类型的常量。
  • .......

第一阶段的验证远不止这些,该阶段的主要目的是保证输入的字节流能正确的解析并存储于方法区内。这阶段的验证是基于二进制字节流进行的,只有经过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,因此后面的3个验证阶段所有是基于方法区的存储结构进行的,不会再直接操做字节流。

元数据验证

元数据验证是对字节码描述的信息进行语义分析,确保其描述的信息符合 Java 语言规范的要求,这个阶段可能包括的验证点以下:

  • 这个类是否有父类(出了 java.lang.Object 以外,全部的类都应当有父类)。
  • 这个类是否继承了不容许被继承的类(被 final 修饰的类)。
  • 若是这个类不是抽象类,是否实现了其父类或接口之中要求实现的全部方法。
  • 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不一样等)。

字节码验证

字节码验证将对类的方法体进行校验分析,保证被校验类的方法在运行时不会作出危害虚拟机安全的事件。

  • 保证任意时刻操做数栈的数据类型与指令代码序列都能配合工做。
  • 保证跳转指令不会跳转到方法体之外的字节码指令上。
  • 保证方法体中的类型转换是有效的。

若是一个方法经过了字节码验证,也不能说明其必定就是安全的。

符号引用验证

符号引用验证能够看作是对类自身之外(常量池中的各类符号引用)的信息校验,一般须要校验一下内容:

  • 符号引用中经过字符串描述的全限定名是否能找到对应的类。
  • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
  • 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。

若是一个类没法经过符号引用验证,那么将会抛出一个java.lang.IncompatibleClassChangeError异常的子类,如常见的java.lang.IllegalAccessErrorjava.lang.NoSuchFieldErrorjava.lang.NoSuchMethodError等。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些类变量所使用的内存都将在方法区中进行分配。此处须要明确类变量的含义,即被static修饰的变量,而不包括实例变量,实例变量会在初始化阶段随着对象一块儿分配在 Java 堆中。此时分配的初始值是数据类型的零值,并非咱们定义的初始值。此处还要明确一个概念,若是变量被final修饰,则此字段的字段属性表存在 ConstantValue 属性,那么在准备阶段变量就会被初始化为 ConstantValue属性所指定的值。可经过下例代码来对照理解:

public static int a = 10;
public static final int B = 20;

其部分汇编字节码为:

Constant pool:
  #2 = Fieldref           #3.#21         // tech/techstack/blog/Test.a:I
  #3 = Class              #22            // tech/techstack/blog/Test
  #5 = Utf8               a
  #6 = Utf8               I
  #21 = NameAndType        #5:#6          // a:I
  #22 = Utf8               tech/techstack/blog/Test


public static int a;
  descriptor: I
  flags: ACC_PUBLIC, ACC_STATIC

public static final int B;
  descriptor: I
  flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
  ConstantValue: int 20

static {};
  descriptor: ()V
  flags: ACC_STATIC
  Code:
    stack=1, locals=0, args_size=0
       0: bipush        10
       2: putstatic     #2                  // Field a:I
       5: return
    LineNumberTable:
      line 8: 0

从上述代码能够看出,B 字段对应的 field_info 与 a 字段对应的 field_info 相比对了一个 Constant_Value 属性,而 Constant_Value 属性的值 20 就会在准备阶段直接赋给字段 B。同时在字节码第 19 行有一个 static {};方法,此方法对应的就是类的构造方法<clinit>在初始化阶段执行,它的Code属性中对应的字节码指令bipush 10为往操做数栈压入 10,putstatic 则是将值 10 赋值给 a 字段。

基本数据类型的零值:

数据类型 零值
byte (byte)0
char '\u0000'
short (short)0
boolean false
int 0
long 0l
float 0.0f
double 0.0
reference null

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用在 Class 文件中以 CONSTANT_Class_info、CONSTANT_Feildref_info、CONSTANT_Methodref_info 等类型的常量出现,具体能够参考博文字节码文件结构详解。此处有符号引用和直接引用两个概念须要了解一下.

符号引用(Symbolic References)

符号引用以一组符号来描述所引用的目标,符号引用能够是任何形式的字面量,只要使用能无歧义地定义到目标便可。符号引用与虚拟机实现的内存布局无关,引用的目标不必定已经加载到内存中。各类虚拟机实现的内存布局能够各不相同,可是它们能接受的符号引用必须都是一致的。符号引用的字面量形式须要明确的定义在 Class 文件格式中。

直接引用(Direct References)

直接引用能够是直接执行目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不一样的虚拟机实例上翻译出来的直接引用通常不会相同。若是有了直接引用,那引用目标一定已经出如今内存中。

关于符号引用和直接引用两个概念看起来很空洞,此处放一个 R 大的回答:传送门

虚拟机规范并未规定解析发生的具体时间,只要求在执行anewarraycheckcastgetfieldgetstaticinstanceofinvokedynamicinvokeinterfaceinvokespecialinvokestaticinvokevirtualldcldc_wmultianewarraynewputfieldputstatic这 16 个用于操做符号引用的字节码指令以前,先对他们所使用的符号引用进行解析。因此虚拟机实现能够根据须要来判断究竟是在类被加载和加载时就对常量池中的符号引用进行解析仍是等到一个符号引用将要被使用前才去解析它。

加载过程当中的解析阶段为静态的将符号引用替换为直接引用的过程。可与虚拟机栈内存中的动态连接参照记忆。

解析动做主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行,分别对应于常量池的CONSTANT_Class_infoCONSTANT_Fieldref_infoCONSTANT_Methodref_infoCONSTANT_InterfaceMethodref_infoCONSTANT_MethodType_infoCONSTANT_MethodHandle_infoCONSTANT_InvokeDynamic_info 7种常量类型。

初始化

初始化阶段是类加载过程当中的最后一步,此阶段才是真正执行类中定义的 Java 程序代码。初始化阶段和准备阶段的初始化是不一样概念的,准备阶段的初始化是给类字段赋值零值的过程,而类加载过程当中的初始化阶段能够看作是类对象的初始化。对于类的初始化反映到字节码中就是类的<clinit>()方法。从另一个角度来说,能够将初始化阶段理解成是执行类构造器<clinit>()方法的过程。同时对于<clinit>()方法,有几个概念要弄清楚。

  • <clinit>()方法是由编译器自动收集类中的全部类变量的赋值动做和静态语句块(static {})中的语句合并产生的。
  • 编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块以前的变量,定义在它以后的变量,在静态语句块中能够赋值,但不能访问。
  • 在执行子类的<clinit>()方法以前,虚拟机会确保子类的<clinit>()方法已经执行完毕。所以在虚拟机中第一个被执行的<clinit>()方法的类确定是java.lang.Object。
  • 接口中不能使用静态代码块,但仍有变量初始化的赋值操做,所以接口也会生成<clinit>()方法。与类不一样的是,执行接口<clinit>()方法不须要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。接口的实现类在初始化时也不会执行接口的<clinit>()方法。
  • 在多线程的状况下,虚拟机会保证一个类的<clinit>()方法只被一个线程调用,其它线程会被阻塞。同时,在一个类加载器下,一个类的<clinit>()方法只会被执行一次。

注:

本文所说的类对象与类实例不是一个概念。关于类对象与类实例以及 java.lang.Class 对象之间的关系,此处能够引用 R 大的一个回答传送门

在HotSpot VM中,对象(类的实例对象)、类的元数据(InstanceKlass)、类的Java镜像(java.lang.Class 实例),三者之间的关系是这样的:

Java object      InstanceKlass       Java mirror
[ _mark  ]                          (java.lang.Class instance)
[ _klass ] --> [ ...          ] <-\              
[ fields ]     [ _java_mirror ] --+> [ _mark  ]
          [ ...          ]   |  [ _klass ]
                             |  [ fields ]
                              \ [ klass  ]

每一个Java对象的对象头里,_klass字段会指向一个VM内部用来记录类的元数据用的InstanceKlass对象;InsanceKlass里有个_java_mirror字段,指向该类所对应的Java镜像——java.lang.Class实例。HotSpot VM会给Class对象注入一个隐藏字段“klass”,用于指回到其对应的InstanceKlass对象。这样,klass与mirror之间就有双向引用,能够来回导航。这个模型里,java.lang.Class实例并不负责记录真正的类元数据,而只是对VM内部的InstanceKlass对象的一个包装供Java的反射访问用。

经过上面的引用,能够清楚的知道 Java Object, InstanceKlass, Java mirror(java.lang.Class instance)在内存中的分布了。

对于初始化阶段能够经过代码来理解一下:

public class SuperClass {

    public static int superClassField = 1;

    static {
        System.out.println("supper class static code");
    }

    public SuperClass() {
        System.out.println("supper class constructor");
    }
}

public interface SuperInterface {
    int superInterfaceField = 10;

}

public class SubClass extends SuperClass implements SuperInterface {

    public static int subClassField = 20;

    static {
        System.out.println("sub class static code.");
    }

    public SubClass() {
        System.out.println("sub class constructor");
    }
}

public class TestClassLoad {
    static {
        System.out.println("test class load");
    }
}

public class App {
  
    static {
        System.out.println("App main static code");
    }
  
    public static void main(String[] args) {
        System.out.println(SubClass.superClassField);
        System.out.println("----------------------");
        new Thread(SubClass::new).start();
    }
}

在运行 SubClass 的时候加上 -XX:+TraceClassLoading 参数,打印出来运行过程当中加载的类。上述代码运行结果为结果 1:

// 类加载日志(节选)
[Loaded tech.stack.App from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]
[Loaded tech.stack.SuperInterface from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]
[Loaded tech.stack.SuperClass from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]
[Loaded tech.stack.SubClass from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]

App main static code
supper class static code
1
----------------------
sub class static code.
supper class constructor
sub class constructor

注释掉new Thread(SubClass::new).start();从新运行程序,获得一下输出结果 2:

[Loaded tech.stack.App from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]
[Loaded tech.stack.SuperInterface from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]
[Loaded tech.stack.SuperClass from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]
[Loaded tech.stack.SubClass from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]

App main static code
supper class static code
1
----------------------

而后将 System.out.println(SubClass.superClassField); 替换为 System.out.println(SubClass.subClassField); 再次运行程序,获得输出结果 3:

[Loaded tech.stack.App from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]
[Loaded tech.stack.SuperInterface from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]
[Loaded tech.stack.SuperClass from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]
[Loaded tech.stack.SubClass from file:/Users/chenjianyuan/IdeaProjects/course/target/classes/]

App main static code
supper class static code

sub class static code.
20
----------------------

这几段代码信息量很大,根据上文所讲慢慢分析:

  • 加载
    • 从类加载日志从看到TestClassLoad类始终都没有被加载。而AppSuperInterfaceSuperClassSubClass 始终被加载,是否是能够证实属于 Applicatin 做用域范围内的类会在首次使用时加载。
    • 对比结果 1 和结果 2 以及没有显示调用SuperInterface任何方法、变量能够看出对于子类来讲,在加载子类时首先要加载实现的接口以及父类。
  • 初始化
    • 当虚拟机启动时,用户须要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

    • 结果 1 结果 3 都代表父类的<clinit>()方法在子类<clinit>()方法以前调用。

    • 结果 1 结果 2 对比代表经过子类调用父类的静态的变量只会引发父类的初始化并不会使子类初始化。

    • 对比结果 1 和结果 2 说明在多线程的状况况下只要类加载器相同,类只初始化一次。

    • 对比结果 一、二、3 能得出一个实例的初始化顺序

      1. 父类 static 代码块即父类的<clinit>()方法。
      2. 子类的 static 代码块即子类的<clinit>()方法。
      3. 父类的构造方法即父类的<init>()方法。
      4. 子类的构造方法<init>()方法、

注:

  1. 关于类实例的初始化过程即对象的实例化过程会专门在另外一篇博客进行讲解。

  2. 关于"接口中不能使用静态代码块,但仍有变量初始化的赋值操做,所以接口也会生成<clinit>()方法。" 在接口中变量初始化赋值操做可参考以下代码:

    public interface SuperInterface {
        int superInterfaceField = 10;
    
        SuperClass su = new SuperClass();
    
    }
    
    // bytecode
    
    {
      public static final int superInterfaceField;
        descriptor: I
        flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
        ConstantValue: int 10
    
      public static final tech.stack.SuperClass su;
        descriptor: Ltech/stack/SuperClass;
        flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    
      static {};
        descriptor: ()V
        flags: ACC_STATIC
        Code:
          stack=2, locals=0, args_size=0
             0: new           #1                  // class tech/stack/SuperClass
             3: dup
             4: invokespecial #2                  // Method tech/stack/SuperClass."<init>":()V
             7: putstatic     #3                  // Field su:Ltech/stack/SuperClass;
            10: return
          LineNumberTable:
            line 10: 0
    }

类的加载时机

关于类在何时加载,咱们能够有上面的代码窥见一斑。可是这只是在JDK1.8, Hotspot 虚拟机测试的状况下得出的结论,也不必定会是正确的,由于 Java 虚拟机规范中并无进行强制约束,关于加载阶段,都是根据虚拟机的具体实现来自由把握。可是对于初始化阶段,虚拟机严格规定了有且只有 5 种状况必须当即对类进行初始化(而加载、验证、准备天然须要再次以前开始):

  1. 遇到newgetstaticputstaticinvokestatic这4条字节码指令时,若是类没有进行过初始化,则须要先触发其初始化。生成这4条指令的最多见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候(已经过上文代码验证)。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,若是类没有进行过初始化,则须要先触发其初始化。
  3. 当初始化一个类的时候,若是发现其父类尚未进行过初始化,则须要先触发其父类的初始化(上文代码也已验证)。
  4. 当虚拟机启动时,用户须要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类(上文代码也已验证)。
  5. 当使用JDK 1.7的动态语言支持时,若是一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且这个方法句柄所对应的类没有进行过初始化,则须要先触发其初始化。

尝试着补充解释一下这几条其中的原理,对于 new 关键字天然不用多说,new 关键字实例化类的实例对象以前天然会执行类的初始化操做,以完成 Java 程序对类的一些操做。getstatic putstatic 指令的含义为读取或设置一个类的静态字段,此处仍是应用R大的回答,原文与上处引用出自同一处:

从JDK 1.3到JDK 6的HotSpot VM,静态变量保存在类的元数据(InstanceKlass)的末尾。而从JDK 7开始的HotSpot VM,静态变量则是保存在类的Java镜像(java.lang.Class实例)的末尾。假若有这样的A类:

class A {
static int value = 1;
}

那么在JDK 6或以前的HotSpot VM里:

Java object      InstanceKlass       Java mirror
[ _mark  ]                          (java.lang.Class instance)
[ _klass ] --> [ ...          ] <-\              
[ fields ]     [ _java_mirror ] --+> [ _mark  ]
             [ ...          ]   |  [ _klass ]
             [ A.value      ]   |  [ fields ]
                                 \ [ klass  ]

能够看到这个A.value静态字段就在InstanceKlass对象的末尾存着了。而在JDK 7或以后的HotSpot VM里:

Java object      InstanceKlass       Java mirror
[ _mark  ]                          (java.lang.Class instance)
[ _klass ] --> [ ...          ] <-\              
[ fields ]     [ _java_mirror ] --+> [ _mark   ]
             [ ...          ]   |  [ _klass  ]
                                |  [ fields  ]
                                 \ [ klass   ]
                                   [ A.value ]

能够看到这个A.value静态字段就在java.lang.Class对象的末尾存着了。

据此咱们应该就能得出结论,在设置静态变量的时候已经须要根据InstanceKlass生成java.lang.Class对象了,而静态变量已经不能在方法区经过读取类元信息进行获取或者储存。而生成 Java mirror 必然要经过完整的类元信息,所以须要进行初始化动做。对于java.lang.reflect包的反射方法,其根据的就是 java.lang.Class对象。对于子类初始化时,由于 Java 的继承特性,继承的是父类完整的类信息。父类进行初始化也是理所固然的。

上述 5 种场景中的行为称为对一个类的主动引用。除此以外,全部的引用类的方式都不会触发初始化,称为被动引用。例:

  • 经过子类调用父类的静态字段(变量+常量),不会致使子类的初始化。代码可参考上文。

  • 经过数组定义来引用类,不会触发此类的初始化

    public class App {
    
        public static void main(String[] args) {
          SuperClass[] superClasses = new SuperClass[10];
        }
    }
  • 常量在编译阶段会存入调用类的常量池中,本质上并无直接引用到定义常量的类,所以不会触发定义常量的类的初始化。

    public class ConstantClass {
        public static final String HELLO_WORLD = "hello world !";
    }
    
    public class App {
    
        public static void main(String[] args) {
            System.out.println(ConstantClass.HELLO_WORLD);
        }
    }

    这是由于虽然在Java源码中引用了ConstClass类中的常量HELLOWORLD,但其实在编译阶段经过常量传播优化,已经将此常量的值"hello world !"存储到了App类的常量池中,之后App对常量HELLO_WORLD的引用实际都被转化为App类对自身常量池的引用了。也就是说,实际上App的Class文件之中并无ConstantClass类的符号引用入口,这两个类在编译成Class以后就不存在任何联系了。能够看一下App的字节码。

    Constant pool:
    	 #4 = String             #25            // hello world !
    	 #25 = Utf8               hello world !
    
    {
     
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=1, args_size=1
             0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #4                  // String hello world !
             5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: return
          LineNumberTable:
            line 10: 0
            line 11: 8
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       9     0  args   [Ljava/lang/String;
    }

例题解析

其实讲到这里,这道题也算是分析完了,那么就根据上面所讲,总结一下这道题:

  1. 运行 Test 类的 main 方法,回顾上文确定要先加载、验证、初始化 Test 类(因为加载、验证必然发生在初始化以前,下面分析就忽略这两个阶段)。
  2. SingleTon.getInstance()Test 类调用 SingleTon 类的静态方法,必然引发 SingleTon 类的初始化。
  3. SingleTon 类存在 singleTon count1 count2 三个静态变量,所以这三个静态变量会被编译器顺序收集值到<clinit>()方法中。
  4. <clinit>() 开始就是 new SingleTon() 会建立 SingleTon 类的实例 singleTon,此时 ``singleTon.count1 singleTon.count2` 值都为 1。
  5. <clinit>() 操做完第一个变量 singleTon 以后即是对第二个变量 count1 操做,此时就会将 1 赋值给 SingleTon 变量 count1
  6. <clinit>() 后续操做即是执行 count2 = 0 即经过操做数栈将 0 赋值给SingleTon 变量 count2

查看SingleTon 的字节码:

{
 static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: new           #5                  // class tech/stack/SingleTon
         3: dup
         4: invokespecial #6                  // Method "<init>":()V
         7: putstatic     #4                  // Field singleTon:Ltech/stack/SingleTon;
        10: iconst_0
        11: putstatic     #3                  // Field count2:I
        14: return
      LineNumberTable:
        line 4: 0
        line 6: 10
}

static{} 方法执行流程正如上文分析。不妨想一下若是将private static SingleTon singleTon = new SingleTon();移动到public static int count2 = 0;下面将会输出什么结果?

总结

类加载

参考:

[1] 周志明.深刻理解Java虚拟机:JVM高级特性与最佳实践.北京:机械工业出版社,2013.

[2] Chapter 5. Loading, Linking, and Initializing

[3] JVM里的符号引用如何存储?

[4] JVM符号引用转换直接引用的过程?

文章首发于陈建源的博客,欢迎访问。
文章做者:陈建源
文章连接: https://www.techstack.tech/post/lei-jia-zai-guo-cheng/

相关文章
相关标签/搜索