大家好,由于最近被动态加载的知识卡住,而动态加载涉及到java虚拟机中的加载机制,因此我决定花一定的时间来学习java虚拟机,特别是类加载部分,主要参照《深入理解java虚拟机》这本书进行学习,这本书的pdf版请前往链接获取。
http://download.csdn.net/detail/hollow12384/9606072
今天是java虚拟机的第一门课程,主要讲解的内容如下:
(1)java虚拟机的语言无关性
(2)Class文件结构概述
(3)解剖Class文件每个字节的含义
java面世时就提出一个口号:”一次编写,到处运行“,这句话表明了java的目标是跨平台运行,这个目标最终怎么实现呢?自然是通过操作系统的
应用层实现,而实现的方式就是虚拟机了。只要不同平台的虚拟机和所有平台都使用统一的程序存储格式—–字节码格式,就能达到跨平台的目的。
这里就涉及到一个概念,什么是字节码格式?
字节码(英语:Bytecode)通常指的是已经经过编译,但与特定机器码无关,需要直译器转译后才能成为机器码的中间代码。字节码通常不像源码一样可以让人阅读,而是编码后的数值常量、引用、指令等构成的序列。
字节码主要为了实现特定软件运行和软件环境、与硬件环境无关。字节码的实现方式是通过编译器和虚拟机器。编译器将源码编译成字节码,特定平台上的虚拟机器将字节码转译为可以直接执行的指令。字节码的典型应用为Java bytecode。
通俗的说,字节码就是源码经过编译器后出来的东西。
字节码本质上就是二进制码。
不过,java的强大之处不止在于跨平台,java不仅具有平台无关性,而且具有很强的语言无关性。什么意思呢?
不管是什么语言,只要对应的编译器能够将程序代码编译成Class文件,java虚拟机并不在乎到底Class文件是由什么得来的,只要它符合Class文件结构的要求,就能在java虚拟机中运行。
前面我们已经介绍了,java虚拟机运行的是Class文件,那么Class文件到底是什么呢?Class文件的结构又该符合什么要求呢?下面讲解的就是这个内容。
首先明确一点,Class文件是一组以8个字节为基础单位的二进制流。Class文件流中没有分割符,这使得Class文件储存的数据几乎都是有效的关键的数据。有人可能会问,如果储存的数据需要占用8个字节以上的空间怎么办呢?这个时候就按照高位在前的方式,分割成若干个8个字节进行储存。
那具体应该怎么储存呢?Class文件格式采用了类似于C语言结构体的伪结构进行储存。这种结构只有两种数据类型:无符号数和表。后面关于Class文件的结构解析都要建立在这两种数据类型上,因此先讲解这两种数据类型。
无符号数属于基本的数据类型,以u1,u2,u4,u8分别代表1个字节,2个字节,4个字节和8个字节的无符号数,无符号数可以用来描述数字,索引引用,数量值,或者按照utf-8编码构成字符串值。
表是由多个无符号数或其他表作为数据项构成的复合数据类型。所有的表都习惯性的用_info结尾,因此可以用这个来区分表和无符号数。整个Class本质上就是一张表,它由以下的数据项构成:
无论是无符号数还是表,如果需要描述同一个类型并且数据量不多,经常会使用一个前置的容量计数器加上若干个数据项的形式,这个时候称这一系列连续的某个类型的数据为某一类型的集合。
第二点说明了Class文件采用无符号数和表来储存数据,但是具体怎么储存呢?储存的规则是什么呢?
在正式讲解之前,建议大家先下载安装一个软件WinHex,我们将使用这个软件来将.class字节码文件转化为16进制数进行解析。
接下来先对Class文件解析的规则进行解释:
整个.class文件转化为16进制后就是按照上述这张表进行解析。
接下来就耐心地对表里的每一项进行详细解释。
在解释之前,由于我比较倾向于用实例解释,因此我用一个实在的java项目生成的.class文件进行解析。
当然,这个java项目十分简单,只有一个类,
这个类位于这个包内:
到这个包所在的目录,打开bin,在里面就可以找到.class文件(注意,我们是对.class文件解析而不是.java文件,还记得吗?进入java虚拟机的是.class文件而不是.java文件)
然后用winHex打开这个.class文件,就可以看到.class文件的16进制表示了。
好了,做足了准备工作,就可以开始解析了。
ps:由于篇幅较长,因此我将这部分分为两篇博客,这篇博客讲解的前四个属性,剩余的属性见JAVA虚拟机入门(1)———-类文件结构(下)
Class文件的前4个字节称为魔数,是用于确定这个文件是否是可以被java虚拟机接受的.class文件,为啥不用后缀名来判断呢?当然是因为
后缀名实在是太容易改了,而文件格式制定者只要采用标示文件格式的魔数,并且魔数没有被其他人采用,那就可以起到标示的作用了。Class文件的魔数是
CAFE BABE(咖啡宝贝),是否特别好记?看我们解析出来的16机制文件前四个字节,正是CAFE BABE!
魔数后面紧跟着的就是版本号了,包括次版本号和主版本号,各占2个字节。在我们的例子中就是0000和0034,0000说明此版本号为0,0034说明主版本号为52,因此编译器jdk的版本就是52.0。
常量池是Class文件中出现的第一个表类型数据类型,而且占据着Class文件的最大空间,同时与其他项目的关联最多。因此,常量池是一个比较复杂并且重要的内容。
大家也可能注意到,在介绍常量池的时候,我用了“不定个字节”,说明常量池的字节长度是不确定的。那怎么确定常量池到底是到哪里呢?这取决于常量池
的前两个字节u2,在这个例子中也就是0016,这表示一共有21个常量,为什么不是22呢(0016的十进制表示就是22)?因为第一个字节是空出来
的,用于后面指向常量池的索引数据在特定情况下表达“不引用任何一个常量池项目”的意思,这种情况下将索引值置为0(也就是常量池第一个字节)就行了。
常量池中的常量主要包括两大类:Leteral和符号引用(Symbolic Reference)。Literal类似于java描述的常量,如文本字符串,final修饰的类型。符号引用主要包括三类常量:
(1)类和接口的全限定名
(2)字段的名称和限定符
(3)方法的名称和限定符
关于类加载的知识在下节解析。
这里抛出一个问题:为什么要使用符号引用?
回到对16进制.class文件的解析,常量池中的每一个常量都是一个表,一共有11种表结构,他们具有共同的特征,就是第一个字节(u1)表示的是这种表的类型,接下来的字节根据他们各自的类型进行解析。主要参考下面的表。
还是举我们的例子,第9个字节和第10个字节(0016)表示常量池一共有21个常量,第一个常量的标志位是07,根据上面的表得知为
CONSTANT_Class_Info(记得之前说过吗,以info结尾的一般就是表结构了),并且u2指定的是全限定名常量项的索引,在这个例子中
u2是0002,说明全限定名常量项在第二个常量中。第二个常量的tag是01,说明是CONSTANT_Utf8_info,u2是0011,说明
utf8字符串占据的字节数是17个字节,也就是从6A一直到下一行的74,这17个字节代表的就是全限定名常量的名字。这里就涉及到怎么翻译utf8缩
略编码了。
从“\u0001”到”\u007f”之间的字符用一个字节表示,”\u0080”到”\u07ff”用两个字节表示,从”\u0800”到”\uffff”用三个字节表示。
在WinHex中,当你选中这17个字节时,旁边会自动显示相应的图形,在这个例子中显示的是”javaLearning/test“,正是这个项目的完整名字!再接下去的解析也是一样的,就不多说了。
不过这里要重点说明一下,由于Class的方法,字段等都需要引用到01,也即是CONSTANT_Utf8_info,因此
CONSTANT_Utf8_info的数量直接决定了java中方法和字段的数量,CONSTANT_Utf8_info指明长度的是length字
段,长度为u2,也就是16个bit,共有65535种可能排序,因此方法数的瓶颈就是65535了!如果方法数超过了这个数,那么将导致无法编译。
常量池21个常量过后就是访问标志了,在这个例子中是在Offset为000000D0这一行的76 61过后的两个字节,也就是00 21。首先先来看访问标志都有什么,以及每个对应的16进制数是什么?
注意表中的每个属性不一定与其他属性相斥,比如ACC_PUBLIC和ACC_FINAL,一个类既可以声明为public,也可以同时声明为final,这个时候的标志值就是两者的或运算了(0001 | 0010)。
回归到我们的例子中,我们的例子只有声明了public,那是不是就是0001呢?请注意,ACC_SUPER说明了,jdk1.2以后编译出来的
类都会有这个标志,而根据前面我们读到的jdk版本号,肯定大于1.2,因此绝对是带有这个标志的。所以真正的标志位为0001 | 0020 =
0021,和我们读出来的结果相符合了。
好了,这就是类文件结构最基本的前面几个字节码了,感兴趣的各位可以看JAVA虚拟机入门(1)——-类文件结构(下)