类文件结构的故事(一)
好久没有更新这系列的文章了,上次更新还是在去年的4月13号,差不多断了一年;
一方面去年那时候忙于学习SSM,准备出来找实习;
一方面实习找到以后,自己也懒惰了,不再像之前那样想啃书了,想来实在无聊,还是读书好,遂重新拿起周志明老师的《深入理解Java虚拟机》这本书
我们已经习惯了写 Java
,习惯了点击运行,然后看到效果,一切都是这么的顺其自然,水到渠成,但是不知道大家有没有想过一件事,我们自己写的Java
文件,编译为字节码文件以后是什么样的,jvm
又是如何执行它的呢?
虚拟机平台
在学习类文件之前,先了解下它的历史;
在二十好几年前,写代码是要看具体平台的,就在前几年,我大一那时候(现在不知道具体如何了,已经好几年没学C语言了,那时候的我,是多么的喜欢C语言啊,还是课代表呢),写 C, C++
也是要看具体平台的,有一个经典的题目,c 语言中,int 占几个字节
,那时候我还年轻啊,学的书本,可能默认是32位平台,书上写的是 4 个字节,然后这一错就错到了期末考试。。
时间继续回到二十好几年前,大家都想有一个与平台无关的语言出现,Sun
公司适时推出了 JVM
,口号喊得很响:一次编写,到处运行 ;
这个虚拟机有许多版本,跑在 32位,16位,64位,跑在 Linux下面,跑在 Windows 下面,但是这些虚拟机都遵循一个标准:使用的程序格式是 字节码文件(class文件)
;
不同平台,不同位,都有自己的指令集,程序员只需要给我 class
文件即可,虚拟机负责做适配,将class
文件,解释成指令;
Sun
在推出 Jvm
之处,就没准备将 Jvm
与 Java
绑定,当时在 Jvm规范
中曾经承诺过,以后 Jvm
在未来会进行适当的扩展,以便支持其他语言,不仅仅局限于 Java
,使得其他语言也可以搭上顺风车;这一承诺目前已经基本算实现了;
可能大家跟我一样,很诧异,Jvm
的 J
难道不是 Java
吗,怎么它还可以执行其他语言呢,因为 Jvm
只与 Class
字节码绑定在一起,而不是与 Java
进行绑定。
java
能够得到运行,也是因为 javac
的功能,将 Java
代码,编译成 Class
文件了,现在市面上,也有一些可以运行在 jvm
上面的语言,比如 JRuby
、Groovy
都得益于它们的编译器,将它们最后都编译为 Class
文件了;
说了这么多,总结如下:
JVM
绑定的是程序存储格式Class
文件JVM
与平台无关JVM
与语言也无关
并且java
语言本身实现的各种语法、变量、关键字,最后都是转换成 JVM
字节码指令,字节码指令的组合可以千变万化,java
也只是使用了其中的一些,一个子集,其他语言可以以这个字节码指令为基础,实现一些 java
本身由于历史原因无法提供友好支持的特性;
类文件的结构
首先先看一句绕口令:任何一个 Class 文件,都对应着 唯一 一个类或者接口的定义信息(java代码
),但是反过来说,任何一个类或者接口的定义信息,并不一定定义在文件里面 ;
书上的这句话是什么意思,说起来有点云里雾里,前半句好理解:任何一个字节码文件,都是从java
代码编译来的。然后跳过 但是反过来说 这句话,后半句的意思就也好理解了:类或者接口,可能并不是定义在本地系统中,可能是从服务器远程加载来的类;
不知道,老师这里 但是反过来说 ,反
对应的是谁,可能是对应的 文件,Class文件
必定存在于本地,但是 Class文件
的定义信息,并不是一定也来自于本地 ;
Class
文件是一组以 8
位字节为基础单位的二进制流,各个数据项之间,不添加任何分隔符,具体的可看下图:
当遇到需要占用 8
位字节以上的空间的时候,则按照大端存储(高位在前)的形式 ,申请若干个 8
位字节 ;
Class
文件采用类似于C语言
里面的伪结构体来存储数据,该伪结构体里面只有两种数据类型:无符号数和表 ;
无符号数 : 属于基本数据类型,用 u1,u2,u4,u8
代表 一个、两个、四个、八个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8
编码构成的字符串值 ;
表 : 是有多个无符号数,或者其他表复合而成的数据结构类型,可以类比于数组;表的名字,默认以 _info
结尾;
整个 Class
文件,本质上可以看作是一张表,里面的数据项如下:
类型 | 名称(数据项) | 数量 |
---|---|---|
u4 | magic(魔数) | 1 |
u2 | minor_version(小版本) | 1 |
u2 | major_version (大版本) | 1 |
u2 | constant_pool_count(常量池中资源数量) | 1 |
cp_info | constant_pool (常量池) | constant_pool_count-1 |
u2 | access_flags(访问标记) | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
这里强调一下:Class
文件的格式很严格,由于它没有任何分隔符的存在,所以 Class
文件的 数据项 ,无论是顺序,还是数量,都严格按照上表描述的那样,不能随意颠倒、缺省;
下面一一讲解这些数据项的意思;
魔数和类文件的版本
魔数,就是确定一个文件的格式的数字,在麻瓜眼中,文件的格式是有文件的后缀名来确定的:.jpg
就是图片 ,.mp3
就是音乐;
其实在魔法师眼中,这些只是一种浮于表面的存在,真正标识文件的东西,由最初的那位创建这个文件的大魔导师选择自己钟爱的数字来标识,只要这个要被选为魔数的数字,还没有被其他魔法师使用过,或者大规模使用;
比如,.class
在初代魔法师刚发明的时候,选择了自己喜爱的 0xCAFEBABE
作为魔数,表示这是一个 .class
文件,魔数,位于文件十六进制的前几位,Class
是前四位字节表示魔数:
从上图中,可以看到魔数(咖啡宝贝);
紧跟着魔数的四个字节,代表了Class
文件的版本号,第5,6
字节是小版本号,第7,8
个字节时是大版本号;
图中,小版本号为 0
,大版本号是 0x34
,也就是十进制的 16*3 + 4 = 52
,这个数字代表 Class
文件的版本。java
的版本号是从 45
开始的,JDK1.0
就是 45
,以后每更新一个大版本号,就增加 1
,那我们现在的 52
,计算下就是 JDK1.8
,也就是这个 Class
文件是 JDK1.8
编译的;
JDK
只能执行 版本号小于等于它对应的版本号的 Class
文件,因此,这个 Class
文件,只能被 JDK1.8
及其以上的版本执行 ;
常量池
紧着着魔数和版本号之后的是常量池入口;
常量池可以看做是 Class
文件的资源仓库,是占用了 Class
文件空间最大的数据项之一,从上面的表中可以看到,常量池是第一个出现的表结构;
由于每个 Class
文件的常量数目不一样,因此,需要有一个标记来统计常量池中的数量个数,紧跟着大版本号之后的两个字节,就代表常量池中常量的数量,这里是 0x0036
,也就是 54
,说明常量池中有 54
个常量;
有 54
个常量,但是这里有个特殊的地方,可供我们使用的索引是 1 -53
,也就是第一个常量,下标为 0
的那个常量,是本来就存在的,这是在类文件创建之初就确定的,和我们没有关系,主要用于表示 某些索引不引用常量池中的任何一个数量项,这时候,只需要把这些索引,指向 0
即可 ;
常量池中主要存放两大类常量:字面量,符号引用;
-
字面量,跟
Java
中的字面量一个意思,诸如文本字符串、final
修饰的常量值; -
符号引用,这个就跟
Java
不搭嘎了,跟 编译原理 挂钩; (博主也没学过,真的做软件,还是要找软工的,其他的都应该归为非科班,不要搞什么计算机相关的,当然这是题外话,我们也可以自己学完软工的专业课);- 类和接口的全限定名
- 字段的名称和描述符号
- 方法的名字和描述符号
保存字面量还可以理解,但是为什么要保存这符号引用些内容呢?因为Java
在编译的时候,没有我们学C
的时候编译-》链接-》执行
中的链接这一步,而是在JVM
加载Class
文件的时候,动态链接的,这也构成了后期绑定,也使得动态成为可能;
因为没有链接这一步,说白了Class
文件中根本就没有保存方法、字段在内存中最终的内存信息,只有在运行时,或者创建类对象的时候,去常量池拿具体的符号引用,解析,然后变为直接引用,指向真实的内存地址;(现在就记住一句话,常量池保存符号引用,是为了后期解析,否则既没有保存真实内存地址,也没有保存符号引用,那还怎么搞。。。)符号引用,在后面要讲的解析过程中,会被转换为直接引用。
常量池中每一个数据项都是表,周志明老师的书是 JDK1.7
,共有 14
种表结构数据,博主的 JDK1.8
共有 17
种
(博主也就按照书上的 14
种来讲,毕竟博主自己有编不出来多出的几个。。):
这些表结构,在表的第一位,都是一个 u1
类型的标记位,代表当前表属于哪一种表结构;并且每种表结构都有自己的数据结构,比如 A
表,有 2
个字段,除了第一个字段,必定是 u1
的标记,代表该表类型,然后还有其他字段,代表各种的意思;关于每种标记对应哪种表,以及每种表中各个数据线的含义,书上给出了关系图,很多,我实在是无法将其写到博客上;
标记位的数字代表哪一种表类型。
作为补偿,以及确保大家能看懂常量池,这里我给推荐一个工具,并且教大家怎么用,用它来快速的查看 Class
文件的结构,而不用我们再去看标记,寻找对应的表,因此书上的关系图,没有,也无所谓啦;
IDEA 插件
该工具是个 IDEA
插件,用于查看类结构:
这里多说一句,可能大家还执着于,我要用眼睛看十六进制的 Class
文件,不要这种,就要看十六进制的文件,那样我才是最牛逼的,其实大可不必,只需要知道,Class
文件是怎么一回事即可,毕竟原始人再怎么厉害,也赶不上我们,道理是这样的吧;
为了打消这种顾虑,这里简单说下,如何用人眼去读 常量池
,上文说的 常量池入口 ,后面紧跟着的就是常量池内容,每一个常量都是一张表,这些表组成了常量池,对每一张表,先看第一位,确定表的类型,然后根据具体类型,具体数据项读取内容;这些内容,有的是引用,有的是字符串,如果遇到引用,则引用的数值(比如 n
),则跳去看常量池中的第 n
张表,继续读;如果是字符串,也就是表 CONSTANT_Utf8_info
,则按照表的规则,读取内容;
说完这些,你也发现,人眼真的不好读取,遇到跳表的时候,鬼知道,那张表在哪里,只能做标记,继续读下一张表,最后串起来;
为此,JDK
提供了工具 javap
,直接可视化类文件 :
当然有了上面我推荐的工具,就不再需要去使用命令行操作了,直接在 IDEA
里面,直接看:
分类查看,更方便!
看下常量池,还支持选择查看:
表已经按照在常量池中的顺序排列好了,具体的值、索引也可以看到:
Java中常量名字的最大占用多少内存
这些表里面, 大部分表都是用来表示引用的,但是那些表示名字的引用最终都会指向CONSTANT_Utf8_info
这张表里面;从名字也可以看出,这张表是保存 UTF8
的信息的,也就是那些表表示引用,引用其他表,最后都要引到这张表,这张表表示具体的信息,常量 (方法名、字段名等等) 的名字;
这张表有个数据项 length
,是 u2
类型,它表示常量名字可以占用的字节数,u2
前面也说过了,就是两个字节,两个字节可以表示的最大数字是 0xffff
,也就是 65535
,也就是常量名字最多可以占用 65535
字节,继续换算,1024字节 = 1KB
,也就是大约相当于 64KB
,也就是,Java
里面变量名字的最大不能超过 64KB
,否则将无法编译,一般谁起名字能起这么多,占用 64KB
,超级玛丽游戏,据说当时也才 32KB
;
常量池中一些莫名其妙的常量
利用可视化工具查看常量池,可以看到有许多不是我们在类中定义的常量,比如 <init>
,这些自动生成的常量,是给下面要讲的 字段表、方法表、属性表
用的,因为上面那些表,最后都指向几个表示数据的表,比如常量名字,就指向 CONSTANT_Utf8_info
,表示 int
就指向 CONSTANT_Integer_info
,这些表的结构,都明确的有相应的固定的数据类型大小;
而 Class
文件中偏偏又许多东西,根本就不是固定字节可以表示的,比如方法的返回值,可以返回任何类型,千变万化,那么用哪张表来表示呢?答案是,哪张表都不能用来表示,毕竟返回值类型千变万化不固定,而表,只是 无符号字节 的一个组合,也就是只是固定结构的表,于是乎,就用那些自动的生成的常量表示;
(有待继续解释,这里其实没说清楚那些不止所云的东西,是从哪来的,为啥存到常量池里面了)
不知云的常量,其实就是符号引用?
常量池就说到这了,面试扯到 JVM
也有点东西可以跟 HR 过几招了;