类加载过程

之前在网上看到一道面试题,很形象的描述了类的加载初始化过程。要完全理解这道题,就不得不深入理解类加载的过程。面试题如下:

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);
    }
}

这道题的正确答案为 :

count1=1

count2=0

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

类的生命周期

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

类的生命周期

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

类的加载过程

加载

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

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

加载途径

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

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

加载方式

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

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

验证

验证时连接阶段的第一步,这一步是为了保证 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 说明在多线程的情况况下只要类加载器相同,类只初始化一次。

    • 对比结果 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/

posted @ 2020-08-14 15:53  41uLove  阅读(138)  评论(0编辑  收藏  举报