文档章节

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

jaffa
 jaffa
发布于 2016/03/26 02:12
字数 2424
阅读 5174
收藏 188
      关于类初始化过程网上有很多相关的文章,其实也算是学习语言时一个基础知识,但今天我想从字节码表现上更深入的来理解各种场景下的实例初始化过程是怎么样的,从简单到复杂大体分为下面几个场景
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,是的就一个,因为静态变量和静态代码块与所在的类被实例化个数无关,而是所在类被虚拟机加载时会执行对应的静态代码块的字节码,这是类被加载事件触发的,并会因为类实例才会有,比如第一次执行下面的非实例化的代码同样会触发该类的静态代码块执行。 注:并非代码中类一出现就会进行加载静态初始化,有些被编译本地化的代码就会不执行,比如使用某个类声明时已经赋值的static+final的属性时。
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的结果。

最后,需要再提的一个是如果一个类有多个构造函数,那么这个类的成员变量实例化和非静态代码块的字节码指令会在所有的构造数中都生成。
 
系列:
 
欢迎转载,但请标明出处: http://my.oschina.net/imcf/blog/647602
参考资料:
为什么能打印null查阅: http://www.tuicool.com/articles/iiYf6vq
 
 

© 著作权归作者所有

上一篇: 图解new
下一篇: Intellij IDEA整理
jaffa
粉丝 27
博文 10
码字总数 9735
作品 0
福州
程序员
私信 提问
加载中

评论(19)

wuyouwei
wuyouwei
@jaffa 哦哦,既然是child,那么一般来说就是设置child的属性name的,难道putfield的原因?
jaffa
jaffa 博主

引用来自“wuyouwei”的评论

引用来自“jaffa”的评论

引用来自“wuyouwei”的评论

大神,请问下右边的字节码文件是怎么弄出来的。。。
你好,感谢关注,首先需要解释是在编译后字节码上看aload_0是指定this对象是肯定的,不同的是aload_0后的指令,在printName()时用的是invokevirtual指令,该指令是个虚调用,说简单些就是会根据aload_0要决定调用谁的printName(),而属性name是通过putfield指令来调用,该指定是直接指向#3连接符,#3代码关Parent.name这个信息,所以putfield是直接将常量“parent”赋给了Parent.name。你可以通过javap -v Parent.class来查看字节码。

@jaffa 你的意思是说在前面设置属性的时候,this表示的是Parent,而后在调用方法的时候,this变为Child?
不是的,这个this就是Child实例
wuyouwei
wuyouwei

引用来自“jaffa”的评论

引用来自“wuyouwei”的评论

大神,请问下右边的字节码文件是怎么弄出来的。。。
你好,感谢关注,首先需要解释是在编译后字节码上看aload_0是指定this对象是肯定的,不同的是aload_0后的指令,在printName()时用的是invokevirtual指令,该指令是个虚调用,说简单些就是会根据aload_0要决定调用谁的printName(),而属性name是通过putfield指令来调用,该指定是直接指向#3连接符,#3代码关Parent.name这个信息,所以putfield是直接将常量“parent”赋给了Parent.name。你可以通过javap -v Parent.class来查看字节码。

@jaffa 你的意思是说在前面设置属性的时候,this表示的是Parent,而后在调用方法的时候,this变为Child?
jaffa
jaffa 博主

引用来自“wuyouwei”的评论

大神,请问下右边的字节码文件是怎么弄出来的。。。
你好,感谢关注,首先需要解释是在编译后字节码上看aload_0是指定this对象是肯定的,不同的是aload_0后的指令,在printName()时用的是invokevirtual指令,该指令是个虚调用,说简单些就是会根据aload_0要决定调用谁的printName(),而属性name是通过putfield指令来调用,该指定是直接指向#3连接符,#3代码关Parent.name这个信息,所以putfield是直接将常量“parent”赋给了Parent.name。你可以通过javap -v Parent.class来查看字节码。
jaffa
jaffa 博主

引用来自“wuyouwei”的评论

大神,请问下右边的字节码文件是怎么弄出来的。。。
javap -v Test.class来输出
wuyouwei
wuyouwei
看完你的分析,我有个疑问,最后一个继承的实例分析,既然aload_0是将局部变量数组中为0坐标的引用this压入栈,我看了下它调用printName()方法前也用aload_0形式给属性name赋值了,如果this是Child,为啥前面为啥不是给Child的属性赋值呢?@jaffa 。。。。。。
wuyouwei
wuyouwei
大神,请问下右边的字节码文件是怎么弄出来的。。。
烫不了大卷
烫不了大卷
图片挂了
pennymei
pennymei
可以试试OneAPM ,他可以为您提供端到端的 Java 应用性能解决方案,支持所有常见的 Java 框架及应用服务器,快速发现系统瓶颈,定位异常根本原因。可以在官网注册试用哦~
jaffa
jaffa 博主

引用来自“YanbinQ”的评论

字节码中的 <cinit> 变成了现在的 static.
这个有助于理解父/子, 类, 实例的初始化顺序. 初始化是有类初始化与实例初始化之分的.
对于有些非 OO 的语言像 C 实现的框架, 只要把 A 实例放在 B 实例的起始位置, 那么 A 就成了 B 的父实例(虽然不是 OO), 因为转型 A a2 = (A)b 是基于首地址的.
关于一开始我也很好奇,在《深入Java虚拟机》书中是以这个为名称的,但是javap后我们看到的是和static{},感觉会直观些
通过Java字节码发现有趣的内幕之String篇(一)

很多时候我们在编写Java代码时,判断和猜测代码问题时主要是通过运行结果来得到答案,本博文主要是想通过Java字节码的方式来进一步求证我们已知的东西。这里没有对Java字节码知识进行介绍,如...

jaffa
2015/08/28
0
7
【JVM系列】一步步解析java执行内幕

对于任何一门语言,要想达到精通的水平,研究它的执行原理(或者叫底层机制)不失为一种良好的方式。在本篇文章中,将重点研究java源代码的执行原理,即从程 序员编写JAVA源代码,到最终形成产...

java菜分享
03/02
0
0
深入理解JVM内幕:从基本结构到Java 7新特

摘要:许多没有深入理解JVM的开发者也开发出了很多非常好的应用和类库。不过,如果你更加理解JVM的话,你就会更加理解Java,这样你会有助于你处理类似于我们前面的案例中的问题。 每个Java开...

开源中国驻成都办事处
2012/12/06
0
1
JVM-ClassLoader

<谭锋>整理 为了支持跨平台的特性,java语言采用源代码编译成中间字节码,然后又各平台的jvm解释执行的方式。字节码采用了完全与平台无关的方式进行描述,java只给出了字节码格式的规范,并没...

项籍20130121
2013/07/11
0
0
升级到JDK9的一个BUG,你了解吗

概述 前几天在一个群里看到一个朋友发了一个demo,说是JDK的bug,昨天在JVM的一个群里又有朋友发了,觉得挺有意思,分享给大家,希望大家升级JDK的版本的时候注意下是否存在这样的代码,如果...

你假笨
2018/06/06
0
0

没有更多内容

加载失败,请刷新页面

加载更多

优雅的关闭Spring Boot

优雅的关闭Spring Boot 1、实现 TomcatConnectorCustomizer 接口拿到Tomcat的连接获取 Tomcat连接池 2、实现 ApplicationListener<ContextClosedEvent> 监听服务器关闭事件,注册JVM钩子函数...

sowhat
今天
2
0
Python3-Web开发

简介 Web开发框架 什么是Web框架? Web应用程序框架或简单的Web框架表示一组库和模块,使Web应用程序开发人员能够编写应用程序,而不必担心协议,线程管理等低级细节。 virtualenv是一个虚拟...

wuxinshui
今天
3
0
使用技媒体实践编写发布博客

技媒体实践博客 CSDN OSChina 知乎 简书 思否 掘金 51CTO

晨猫
今天
2
0
Lucene

1、什么是全文检索 数据分类 我们生活中的数据总体分为两种:结构化数据和非结构化数据。 结构化数据:指具有固定格式或有限长度的数据,如数据库,元数据等。 非结构化数据:指不定长或无固...

榴莲黑芝麻糊
昨天
5
0
python到setuptools、pip工具的安装

python安装 基础开发库   apt-get install gcc  apt-get install openssl libssl-dev 安装数据库和开发库   apt-get install mysql-server libmysqld-dev python环境   下载地址...

问题终结者
昨天
6
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部