有一道非常经典的题目,如果对虚拟机加载类的过程不熟悉,很容易就答错,题目如下:
public class Singleton { public static Singleton instance = new Singleton(); public static int a; public static int b = 0; private Singleton() { a++; b++; } public static Singleton getInstance() { return instance; } public static void main(String[] args) { Singleton s = Singleton.getInstance(); System.out.println(s.a); System.out.println(s.b); } }
首先了解一个概念,主动引用,jvm规范中规定有且只有下面几种才是主动引用,主动引用会触发类的初始化。
1.遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
上面的四条指令都是字节码指令,可以理解为new,获取静态属性,设置静态属性,调用静态方法。
a. new 一个类的时候会发生初始化
b.调用类中的静态成员,除了final字段,看下面这个例子,final被调用但是没有初始化类
这里注意是除了final字段,因为final字段在编译期已经将值存储到了类的常量池中,因此引用final的静态成员是,不会导致初始化动作。
c. 调用某个类中的静态方法,那个类一定先被初始化了
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3.当初始化一个类的时候,如果发现其父类还没进行过初始化,则需要先触发其父类的初始化。
4.当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类。
除了上面四种情况的主动引用,还要注意有三种被动引用并不会触发类的初始化
1.通过子类引用父类的静态字段,不会导致子类初始化
2.通过数组定义类引用类,不会触发此类的初始化
3.常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
因此Singleton在jvm找到main方法入口的时候,便会进行类的初始化动作。
类的初始化包括下面几个步骤:
1.类的加载,由classloader讲二进制文件加载到内存。
2.连接阶段,其中该阶段又分为三个过程
a.验证,验证加载进来的字节码的合法性。
b.准备,为类的静态变量分配内存并初始化为默认值(int为0,double为0.0等等这些,并不是指代码中=后面的值,注意此时类的实例还没有生成,因此不涉及实例变量)
c.解析,将符号引用解析为直接引用。
3.初始化,将类的静态变量初始化为程序中的值。
对于Singleton,在连接阶段的第二步,instance会被赋值为null,a和b会被赋值为0。然后此时进行第三步初始化,在初始化instance的时候也即new Singleton,会执行构造函数,此时a变为1,b变为1,然后再去初始化a,由于没有赋值动作,故a仍然为1,但是在初始化b的时候,b会被重新赋值为0,因此在打印的时候b输出的为0。
因为对于static的初始化是按照定义的顺序进行的,因此如果将public static Singleton instance = new Singleton();放到最后初始化,则打印的a和b都为1。
为了更好的理解上面的过程,通过javap命令将class文件的虚拟指令输出,命令如下javap -verbose -private Singleton > Singleton.txt,执行该命令前请先试用javac编译Singleton。内容如下:
Compiled from "Singleton.java" public class Singleton extends java.lang.Object SourceFile: "Singleton.java" minor version: 0 major version: 50 Constant pool: const #1 = Method #10.#27; // java/lang/Object."<init>":()V const #2 = Field #8.#28; // Singleton.a:I const #3 = Field #8.#29; // Singleton.b:I const #4 = Field #8.#30; // Singleton.instance:LSingleton; const #5 = Method #8.#31; // Singleton.getInstance:()LSingleton; const #6 = Field #32.#33; // java/lang/System.out:Ljava/io/PrintStream; const #7 = Method #34.#35; // java/io/PrintStream.println:(I)V const #8 = class #36; // Singleton const #9 = Method #8.#27; // Singleton."<init>":()V const #10 = class #37; // java/lang/Object const #11 = Asciz instance; const #12 = Asciz LSingleton;; const #13 = Asciz a; const #14 = Asciz I; const #15 = Asciz b; const #16 = Asciz <init>; const #17 = Asciz ()V; const #18 = Asciz Code; const #19 = Asciz LineNumberTable; const #20 = Asciz getInstance; const #21 = Asciz ()LSingleton;; const #22 = Asciz main; const #23 = Asciz ([Ljava/lang/String;)V; const #24 = Asciz <clinit>; const #25 = Asciz SourceFile; const #26 = Asciz Singleton.java; const #27 = NameAndType #16:#17;// "<init>":()V const #28 = NameAndType #13:#14;// a:I const #29 = NameAndType #15:#14;// b:I const #30 = NameAndType #11:#12;// instance:LSingleton; const #31 = NameAndType #20:#21;// getInstance:()LSingleton; const #32 = class #38; // java/lang/System const #33 = NameAndType #39:#40;// out:Ljava/io/PrintStream; const #34 = class #41; // java/io/PrintStream const #35 = NameAndType #42:#43;// println:(I)V const #36 = Asciz Singleton; const #37 = Asciz java/lang/Object; const #38 = Asciz java/lang/System; const #39 = Asciz out; const #40 = Asciz Ljava/io/PrintStream;; const #41 = Asciz java/io/PrintStream; const #42 = Asciz println; const #43 = Asciz (I)V; { public static Singleton instance; public static int a; public static int b; private Singleton(); Code: Stack=2, Locals=1, Args_size=1 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: getstatic #2; //Field a:I 7: iconst_1 8: iadd 9: putstatic #2; //Field a:I 12: getstatic #3; //Field b:I 15: iconst_1 16: iadd 17: putstatic #3; //Field b:I 20: return LineNumberTable: line 10: 0 line 11: 4 line 12: 12 line 13: 20 public static Singleton getInstance(); Code: Stack=1, Locals=0, Args_size=0 0: getstatic #4; //Field instance:LSingleton; 3: areturn LineNumberTable: line 16: 0 public static void main(java.lang.String[]); Code: Stack=2, Locals=2, Args_size=1 0: invokestatic #5; //Method getInstance:()LSingleton; 3: astore_1 4: getstatic #6; //Field java/lang/System.out:Ljava/io/PrintStream; 7: aload_1 8: pop 9: getstatic #2; //Field a:I 12: invokevirtual #7; //Method java/io/PrintStream.println:(I)V 15: getstatic #6; //Field java/lang/System.out:Ljava/io/PrintStream; 18: aload_1 19: pop 20: getstatic #3; //Field b:I 23: invokevirtual #7; //Method java/io/PrintStream.println:(I)V 26: return LineNumberTable: line 21: 0 line 22: 4 line 23: 15 line 24: 26 static {}; Code: Stack=2, Locals=0, Args_size=0 0: new #8; //class Singleton 3: dup 4: invokespecial #9; //Method "<init>":()V 7: putstatic #4; //Field instance:LSingleton; 10: iconst_0 11: putstatic #3; //Field b:I 14: return LineNumberTable: line 4: 0 line 7: 10 }
首先new执行,在堆中生成Singleton的实例,并将指向该实例的指针压入操作数栈(栈帧的组成元素之一,还有一个要了解的是一组局部变量,下标从0开始)中。
dup命令复制操作数栈栈顶的值,注意此时操作数栈有两项值且都为this引用
接下来的invokespecial通过栈顶的this引用调用构造方法,消耗栈顶的this引用。
转到构造函数中,注意有一行Stack=2, Locals=1, Args_size=1所有的方法都有一行类似的数据,其中Stack=2表示操作数栈的长度为2个slot,其中一个slot占用四个字节,Locals=1表示本地变量表长度为1,因为这里用到了this指针,默认方法的本地变量第一个值为this引用,Args_size=1这里表示传入方法的参数,所有的实例方法至少都会是1,因为默认会传入this指针,因此这里的构造函数默认会接收this参数。
aload_0表示将局部变量数组中索引为0的值压栈,这里将this压栈,然后invokespecial调用Object的初始化方法,getstatic方法获取a的值并压栈,这里a为0,然后iconst_1将1压栈,此时栈里有两个值,分别为0和1,iadd弹出栈顶的两个值然后相加并将结果压栈。putstatic将结果1赋值给a,后面类似的操作将结果1赋值给b。初始化方法返回,继续前一个栈帧的执行即static{}块。
putstatic将this引用赋值给instance变量,此时操作数栈为空
iconst_0,将0入栈
putstatic将0赋值给变量b,此时b又变回了0,因此b最终的结果为0。
弄清楚整个问题的关键是掌握jvm对于类变量的初始化过程。首先是为类变量分配内存并初始化为默认值,此为一个阶段,然后是代码本身在构造函数中的初始化并不代表最终的值,因为jvm还会对类变量进行初始化动作,即执行等号动作b=0;该动作会被编译到static{}块中,static块中初始化的顺序和代码中申明的顺序有关,也就是构造函数被调用的顺序影响到最终的值,而构造函数被触发的条件就是new动作的执行。因此最后的输出和public static Singleton instance = new Singleton();的顺序有关。
原文地址:http://blog.csdn.net/tangyongzhe/article/details/43954087