首先
引用一句话“代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步”,这句话蕴含着java的精髓。
本文为记录学习并分享交流之用,我尽量言简意赅,提纲挈领。其中如有不当之处敬请指正。
正文
我们知道C++代码编译出的文件是二进制的机器码,可以直接在本地机器运行。java编译出的是class文件,需要JVM来执行。要理解JVM究竟是如何来加载并执行class文件,我们首先要搞清楚class文件的存储组织结构是什么。
首先,class文件是一连串8位字节排列组成的二进制流。其中包含的所有数据项目紧密排列,没有任何分隔符。
既然是一种紧密排列,没有任何分隔符的二进制流,那么class文件中每个字节表示的含义,一定都是被严格限定的。我们要说明的就是这种“限定规则”。
直观起见,先看一下由一段十分简单的代码编译出的ClassStructure.class文件。代码如下:
public class ClassStructure { private String string = "hello"; public String hello() { return string; } }
将这段代码编译出的ClassStructure.class文件用Notepad++的HEX-Editor插件打开如下,这就是这段代码编译出的字节码所包含的所有信息,如下图:
当然,所有的文件都可以以这样的十六进制方式打开。不过,class文件有一个特征,我们看它的头部四个字节为:ca fe ba be,连在一起就是“咖啡,宝贝”。这头四个字节是用于该文件身份识别的“魔数”,JVM就是通过它来判定该文件是否为能够被虚拟机接受的Class文件的。
有了这个头部,我们来看class文件的具体存储规则:(注意,所有字节依次紧密排列,无分隔符)
先只截取到常量池部分,后面存储的内容我们暂且不管,在后文中会提到。我们这里先把常量池说清楚,也便于理解后面部分的对常量池的“名称索引”究竟是怎么回事。
这里要强调的一点是:**常量池中的常量条目并不是我们直观理解的编程语言中的一个个单个的const常量,而是有点像c语言中“结构”的一种数据结构!我们把这种结构称为“表”。**class文件中的所有数据几乎都是以“表”的形式存放的。在后面还会多次碰到。
我们先看该字节码的前十个字节截取如下:
前十个字节(每个字节为连续两个16进制数)分别为ca fe
ba be
00 00
00 33 00 17
,头部cafe babe已经说过了;后面红色2字节为jdk的次版本号;绿色2字节为jdk的主版本号(这里为十进制的51,高版本可以向下兼容);蓝色部分为常量池中常量项目的数量,这里0x0017表示常量池中一共有1 * 16 + 7 = 23
个常量项目。它们依次排列,索引号依次为0 ~ 22,和编程语言中数组访问方式相同。但是,有一点需要特别说明!第0个常量是空出来用作特殊目的的,也就是我们真正的常量条目是1 ~ 22这22个常量!
我们接下来具体看这22个常量条目,前面说过class文件是紧密排列的字节流,每个字节的具体含义一定要有明确且严格的限定,这样才能保证JVM正确执行。因此常量池中每个常量的大小,组成以及排列方式一定要有明确说明才行!
实际上,常量池中每种常量表的具体存放规则是事先规定好的,JDK 1.7之前有12种。我们首先以两种常量类型为例,先看存储方式,再看具体含义:
1、Sting
类型的常量表存储方式如下,字面意思理解就是描述一个String类型的常量。
2、Methodref
类型常量表存储方式如下,字面意思就是描述一个方法索引(ref)的常量类型。
以上为12种常量中的两种,所有常量类型都用tag的值来唯一标识。每个常量表的头部一定有一个字节的tag来表示其常量类型,而且每种常量的tag值是固定的。例如Methodref
类型的tag值为“10”,Sting
类型的tag值为“8”,由此可以唯一确定该处存放的是哪一种常量表。而每种常量的具体存储方式又是事先规定好的。例如上面提到的Methodref
类型会固定地占用5(1 + 2 +2)
个字节,每个字节的含义见上表,String
类型会占用3(1 + 2)
个字节,每个字节的含义见上表。由此22个常量可以依次紧密排列!并且每个字节的含义十分明确!
啰嗦一句,关于Methodref
常量表中出现的“索引”究竟是什么意思。比如其中指向Class类型常量
的索引。该索引的value,也就是这两个字节里面存储的具体值,是我们上面提到的0~22的索引号。也就是说,它指向常量池中的另一个常量,该常量类型为Class
(12种之一)。
要注意!Methodref
类型常量的“常量表”中只存放了2个索引值,没有该方法的具体描述。该方法的具体描述是依靠索引常量池中的其他常量实现的,我们后面会看到。
前面已经说过,本文中的常量池一共存放了22个常量表,那么从接下来的的第11个字节开始(如下图下划线处)就应为第一个常量表的tag值了。值为0x0a,也就是十进制的“10”,上面说过,tag值为10表示该条目是Methodref
类型。根据Methodref
常量表的规定,接下来的两个字节为指向Class类型的索引号,如图中下划线的0x00 05
。再接下来两个字节为0x00 13
,也就是#19
处(16 + 3)为该方法的具体描述的常量(类型为NameAndType
)。
第一张常量表的5个字节到此结束。接下来应该是下一个常量的tag值,下一个下划线开头处为0x08,也就是String
类型的常量,接下来两个字节0x00 0f
为指向Utf8
类型常量的索引。至此,该常量的3字节完毕,接下来应该是下一个常量的tag值……
一个字节一个字节地手工翻译是学习class文件结构的好方法,但毕竟麻烦。事实上,jdk中的javap工具会帮我们做我们上面做的“翻译”工作,我们通过cmd进入命令窗口,用javap工具翻译刚才生成的ClassStruct.class文件如下:
如上图,第一个常量
#1 = Methodref #5.#19
类型为
Methodref
,和我们自己翻译的一样,它的两个索引分别为#5和#19。我们顺着索引看过去:
#5 = Class #22
Class类型的常量表中存储的也是一个索引,继续顺着索引看过去:
#22 = Utf8 java/lang/Object
到这里,我们找到了声明该方法的类为java/lang/Object。同时也说明了“该方法的具体描述是依靠索引常量池中的其他常量实现的”。实际上,该方法是编译器为我们添加的构造器。至于#19所指向的#8和#9的具体含义,我们下文再说。