码迷,mamicode.com
首页 > 其他好文 > 详细

从一道题目看类加载

时间:2015-02-27 10:08:33      阅读:167      评论:0      收藏:0      [点我收藏+]

标签:java   类初始化   jvm   

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

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

问上面这段代码输出的内容是什么?答案是1,0。如果您能答上来说明您对类加载的过程已经很熟悉了,下面就来分析一下为什么会有上面的结果。

首先了解一个概念,主动引用,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


}

注意最下面的static{}静态块,所有的静态属性都会在该块中被初始化,该初始化对应的就是第三步。

首先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();的顺序有关。

从一道题目看类加载

标签:java   类初始化   jvm   

原文地址:http://blog.csdn.net/tangyongzhe/article/details/43954087

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!