通过Java字节码发现有趣的内幕之初始化篇(下)

关于类初始化过程网上有很多相关的文章,其实也算是学习语言时一个基础知识,但今天我想从字节码表现上更深入的来理解各种场景下的实例初始化过程是怎么样的,从简单到复杂大体分为下面几个场景 。

1、成员+构造函数
2、成员+代码块+构造函数
3、 静态变量+静态代码块
4、 继承和多态

首先明确下运行环境:

 

我们先来看一下第一个场景: 成员+构造函数,也是我们最经常使用到的场景。在这个场景中想通过字节码了解下成员属性的初始情况,下图左边代码 声明了四个类成员属性和一个无入参构造函数 ,右边是对应的执行字节码栈帧执行顺序。

1、在字节码(1)处隐示的调用了类的父默认构造函数,这个很重要,决定了类的多重初始化过程,详细在最后一个场景展开;

2、代码中myId1和myId2属性声明方式不同,初始化过程也是不一样的,如图中所示,myId1在声明属性时未进行任何的指令操作,而是等到构造函数中的myId1=100时才有执行指令,而像myId2在声明就进行赋值指令,所以myId2会被myId2优先初始化。

3、myText2在声明时赋于null,所以我们可以看到指令也会进行aconst_null的操作,但是在(6)时再次对myText2进行了赋值并再次产生了指令操作。注:null本身不是一种对象,在JVM中没有明确的指明采用什么类型,不同的JVM实现可能不一样,我们可以简单理解为null是一个标志,告诉虚拟机对应的类型还不明确,并还未为其分配空间。

4、从这个场景图的右边字节码指令执行过程我们可以总结出:
  • 有赋值的类 成员属性是按声明的位置先后进行初始化(与访问标志符无关),如图(2)(3);
  • 成员属性的初始化会优先于构造函数的初始化,如图(3)(6);
  • 初始化动作都是在构造函数中完成的, 如果没有显示构造函数,那么编译器会产生一个无入参构造函数来完成初始工作;
  • 建议声明成员属性时没有必要赋于null,等到真实需要使用成成员时再初始化或传递值;

再来看第二个场景,如果我们的类中有非静态的代码块时,整体初始化是怎么样的,先来看下面的代码示例两个print方法最终会输出什么内容。

最终会输出:

text:null
text:text1-1

我们从字节码上分析一下为什么会是这个输出结果。

1、从图的字节码上可以看出,非静态代码块的执行最终也是被放进构造函数中完;

2、代码块与成员属性的初始化顺序也是按其在代码中出现的先后顺序,如(2)(3)所示;

3、所以在执行(1)print时,实际上myText1还没有被任何的初始化,包括成员属性的赋值,所以这时输出text:null;

4、实际上 myText1是在(2)才有值,但紧接着还会被(3)给替换,所 在执行(4)时输出text:text1-1;

5、该场景总结:

  • 非静态代码块的执行也是被放到构造函数中。
  • 非静态代码块并不影响代码顺序的初始化工作
  • 尽量不要有非静态的代码块,可读性不好,需要在非静态代码块解决的问题完全可以移到构造函数中。

接下来我们来看第三个场景,同样我们先来看一个代码输出结果 ,下面的代码最终输出的什么?

我们再来一下静态变量与静态代码段的字节码是什么样的:

1、从图上我们发现静态量和静态代码块编译后都整合到一个static段中,如(1)(2)(3),这个段中的字节码执行顺序就是静态变量和静态代码块在源代码中的出现顺序,而且该static{}最终在虚拟机加载类时调用一次;

2、而(4)(5)虽然实例化两个对象,但与static{}里的执行码没有任何的关系;

3、静态变量和静态代码块的声明顺序决定了引用顺序,比如图上代码中的(6)是一个不合法的引用,因为myStaticId2在其后面声明;

4、总结 :上面执行结果是一个200,是的就一个,因为静态变量和静态代码块与所在的类被实例化个数无关,而是所在类被虚拟机加载时会执行对应的静态代码块的字节码,这是类被加载事件触发的,并会因为类实例才会有,比如第一次执行下面的非实例化的代码同样会触发该类的静态代码块执行。

System.out.println(StaticFieldInitialize.class);
//或
Object obj = new Object();
if(obj instanceof StaticFieldInitialize){

}

最后一个场景继承的初始化过程,如果理解上面三个场景后,继承可以拆成下面两个步骤来看初始化,第一步执行父类的初始化过程,第二步执行本身类初始化过程,如果父类还有父类重复这两个步骤,而每一个步骤都遵循下面过程:

1、一性次:优先加载类时会初始化静态成员/静态代码块 ,顺序为父类 -》子类,每个类在JVM只会被初始化一次,除非类被卸载再加载;
2、接着会按继承关系执行:父类成员属性/非静态代码块 -》父类构造函数 -》成员属性/非静态代码 -》 构造函数;

但是说到继承我们更多的时候会考虑到多态的过程,下面一起来分析理解动态的执行场景,同样先来看两段代码,如下图:

这是一个非常简单继承关系,Chlid继承了Parent类并重写了printName方法,是个典型的多态特性,那么在执行Child类的mian方法后会输出什么呢?答案是输出child = null, 如果你已经充分理解了多态性那么你可能很短的时间也得出这个答案,但是这个过程是怎么样的呢,我们还是一步一步分析下:

1、在第一个场景中我们知道在Child类初始化自己构造函数时,第一步会优先调用它的父类构造函数,而这时Child类的name属性还未被赋值,即它还是null;
2、在执行父类构造函数时,调用Object构造函数后,先执行Parent类的name成员赋值的字节码,紧张着会调用printName方法,但从源代码层面看这似乎就是调用Parent的printName方法,但是事实非如此,我们看一下Parent类的字节码。

从字节码上我们可以看到,在调用printName的前一个栈帧为 aload_0指令,这个指令是指将 当前的局部变量数组中下标为0 的引用压进栈,而所有的实例化的对象局部变量数组下标为0的引用都是this,而this这个关键字与运行时多态紧密相关,也就是说你在代码中看到的this在运行时有可能不代码当前表实例本身,有可能是子类传递的引用,我们可以通过debug方式进一步验证,我在Parent类构造函数调用printName处下个断点进行debug。

从上面运行过程的图中可以发现,此时在Parent类中的this并不是代码Parent实例本身,而是代表Child类的实例引用,所以此时运行调用时会优先到Child实例上去寻找printName方法,如果有该方法就执行,没有则执行父类的。

3、当通过运行时动态调用Child实例printName方法时,Child类的name属性还未初始化,所以看到输出 child = null的结果。

最后,需要再提的一个是如果一个类有多个构造函数,那么这个类的成员变量实例化和非静态代码块的字节码指令会在所有的构造数中都生成。

参考资料:

关于null详解请查阅: http://stackoverflow.com/questions/2707322/what-is-null-in-java/
为什么能打印null查阅: http://www.tuicool.com/articles/iiYf6vq
字节码指令请查询: http://blog.csdn.net/lm2302293/article/details/6713147

本系列:



相关文章

发表评论

Comment form

(*) 表示必填项

1 条评论

  1. loveyzhou 说道:

    “Parent类中的this并不是代码Parent实例本身,而是代表Child类的实例引用”,想知道这个this是怎么传递的?

    Thumb up 0 Thumb down 0

跳到底部
返回顶部