类文件结构的故事(一)


好久没有更新这系列的文章了,上次更新还是在去年的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 之处,就没准备将 JvmJava 绑定,当时在 Jvm规范 中曾经承诺过,以后 Jvm 在未来会进行适当的扩展,以便支持其他语言,不仅仅局限于 Java,使得其他语言也可以搭上顺风车;这一承诺目前已经基本算实现了;

可能大家跟我一样,很诧异,JvmJ 难道不是 Java 吗,怎么它还可以执行其他语言呢,因为 Jvm 只与 Class字节码绑定在一起,而不是与 Java 进行绑定。

java 能够得到运行,也是因为 javac 的功能,将 Java代码,编译成 Class 文件了,现在市面上,也有一些可以运行在 jvm 上面的语言,比如 JRubyGroovy 都得益于它们的编译器,将它们最后都编译为 Class 文件了;

说了这么多,总结如下:

  1. JVM 绑定的是程序存储格式 Class 文件
  2. JVM 与平台无关
  3. 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 即可 ;

常量池中主要存放两大类常量:字面量,符号引用

  1. 字面量,跟 Java 中的字面量一个意思,诸如文本字符串、final 修饰的常量值;

  2. 符号引用,这个就跟 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 过几招了;

posted @ 2019-04-01 10:33  Yiaz  阅读(155)  评论(0编辑  收藏  举报