有一道非常经典的题目,如果对虚拟机加载类的过程不熟悉,很容易就答错,题目如下:
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