JVM学习日记⭐️4000字的小白也能懂的Class类文件结构(上)⭐️

741 阅读15分钟

🔉引言

代码编译的结果从本地机器码过渡到字节码,看起来是存储格式进步的一小步,但却是编程语言发展的一大步,我们的计算机从出生到发展这么多年依然不具有普遍意义上的智能,依然只认识01,这使我们仍然要把java程序编译成机器码才能交给计算机去运行,但由于虚拟机及大量建立在虚拟机上的语言的发展,使机器码并不是我们唯一的选择,越来越多的程序语言选择了与操作系统无关的、平台中立的格式作为程序语言编译后存储的格式。

如果全世界的计算机的指令集只有x86一种,操作系统只有windows一家独大的话,那java语言就没有出现的必要了,当初创建java语言的初衷就是一次编写,到处运行。在操作系统的应用层上实现可以建立在不同平台和不同硬件上的java虚拟机,这些虚拟机都可以载入和运行不属于这个平台的字节码,从而实现这种打破平台限制的欲望,真正实现一次编写,到处运行。

字节码

字节码是不同平台都统一支持的存储格式,实现语言无关性的基石是字节码(Byte Code),java虚拟机只与Class文件进行关联,既然有关联,那Class文件就必须包含java虚拟机的指令集、符号表等其他辅助信息。同时出于安全考虑,《java虚拟机规范》强制规定了许多语法和结构化的约束。

java语言中的各种符号变量、语法、关键字等都是由多条字节码指令组合而成,这就必须让字节码的表述能力要比java语言本身还要强大,因此java语言表述不出来的语言特性不代表字节码也表述不出来,这也为其他程序语言实现有别于java语言的特性提供了空间。

Class文件的结构

我记的我第一次阅读这个内容的时候,感觉就跟读有字天书一样,特别无趣,也不知道干什么,但是这又是很重要的java虚拟机的基础之一,是了解java虚拟机的必经之路,没有捷径,你要想比较深入学习java虚拟机的知识,就必须坚定的走下去。

java虚拟机之所以能保持非常良好的向后兼容性,Class文件的稳定性的结构功不可没,第一部的《java虚拟机规范》是在1997年公布的,但时至今日,经过了十几个版本的迭代,那时定义的各项细节几乎没有发生任何改变,就算改变的部分也是在原有的基础结构上扩展内容,并未对已经定义出的内容做任何修改。

CLass文件是一组以8个字节为单位的二进制流,各个数据项严格的按照顺序排列,中间没有分割符,没有空隙存在,那要是遇到超过8位的数据项呢,那就按照高位在前,按8位一组进行存储。

java虚拟机规范》规定:Class文件采用类似于C语言结构体的伪结构进行存储,这中数据结构只包含两个类型:无符号数和表,这是比较重要的两个基础元素,所以必须要搞明白。

什么是无符号数呢?无符号数就是一个数据的基本类型,以u1、u2、u4、u8分别表示1、2、4、8个字节的无符号数,可以用来描述数字、索引引用、数量值或按Utf-8表示的字符串的值。

什么是表呢?表是由多个无符号数和其他的表组成的混合的一种数据类型,为了便于区分,所有表的命名都采用_info来结尾,整个Class文件本质上也可看成是一张表。

无论是无符号数还是表,当需要描述同一个类型,但数据个数不确定的时候,那就要采用集合的方式,即一个前置的容量计数器加若个连续的数据项的表现形式。

那Class文件并不像我们熟悉的XML等文件格式,由于它没有分隔符,这就导致了要存储的数据项无论在顺序上还是在数量上亦或是数据存储的字节顺序等信息都要被严格定义,严格到什么程度呢?哪个字节什么含义,长度多少,先后顺序都是不允许更改的。

Class文件中的魔数

每个Class文件的头4个字节被称为魔数,也就是我们熟悉的0xcafebabe,使用魔数作为标识当然是出于安全方面的考虑,事实上不光是java,我们熟悉的图片也都有类似的魔数。图如下:

image.png

Class文件中的版本号

紧跟着魔数后面的就是Class文件中的版本号,那我们的java8版本就是52.0,为了显示我不是吹牛,我们上代码。

image.png

5和第6个字节是次版本号,第7和第8个字节是主版本号

image.png

常量池

接下来就到了常量池的入口处了,由于常量池中常量的数量是不确定的,因此呢入口处放置一项U2的数据,代表常量池容器计数值,我们可以看到常量有18个,索引值范围是1-18,0是空出来的,如果后面某些指向常量池的索引数据不引用任何一个常量池的常量时,就可以用0索引值来标识。

image.png

常量池中主要存储两大常量:字面量和符号引用,字面量类似于Java语言层面的常量,而符号引用属于编译原理一块的内容。主要有以下常量:

  1. 被模块导出或者开放的包(Package
  2. 类和接口的全限定名(Fully Qualified Name
  3. 字段的名称和描述符(Descriptor
  4. 方法的名称和描述符、
  5. 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic
  6. 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant

需要注意的是我们的Class文件不会存储任何方法、字段在内存布局中的信息,也就是说是无法被直接使用的,那怎么玩呢?肯定是需要将常量池中的符号引用在实际类加载的过程中,在类的创建和解析时,替换为实际的内存地址。

常量池中的每一个常量都对应一个常量表,还记得表的后缀是_info嘛,对接下来你会看到大约17info为后缀名的表,表如下:。

常量池的项目表

image.png

image.png

我的妈耶,妈妈喊我回家吃饭,先溜了。。。。。

吃饱了回来了,这些常量各个都不同,都有着自己独立的数据结构,并且两两之间还没有什么联系,你要是想通用的介绍,那是连门都不会有的,所以只能一个一个摸,先看看谁比较幸运。

所以光有项目表还是不够,需要把结构表也搬过来。

项目结构表

image.png

image.png

image.png

我们走到了常量入口,一看是oa,就是10,查项目表得是CONSTANT_Methodref_info代表类中方法的符号引用,再查结构表,就这么对应着查,tag10,这个tag呢是一个区分常量类型的标志位,然后一看占两个字节,index04,指向常量池中CONSTANT_Class_info常量,然后另一个index15,指向常量池中的CONSTANT_NameAndType类型常量。

image.png

然后,我们来验证一下,看看是否是吹牛逼,怎么验证呢?我们可以输入指令:javap,图如下:

image.png

牛不牛比,哈哈,是不是很有趣?但是看这张图确实知道#加数字对应常量池的引用,但是没有数字的是咋肥事呢?比如I、V、、LineNumberTable、LocalVariableTable,这一部分确实不是java代码搞的,而是编译器自动搞的,这些值会被字段表、方法表、属性表所引用。用来描述返回值啊,参数类型和个数啊等等,因为java类,那是无穷无尽的啊,所以就不能采用无符号数去表示,不然得写多么长,那当方法需要描述这些信息时,直接引用常量表中的符号引用即可,先简单理解后续会详细描述。

访问标志

常量池,终于结束了,但是我们的旅程还没结束,常量池后面紧跟着访问标志,就是看看你的Class是类还是接口,访问权限如何?是abstract嘛?是否被final盯上了呢?访问标志如下表:

访问标志表

image.png

访问标志一共16个,当前只能看到9个,我们这个测试类只是一个普通的类,只用到两个访问标志,值为201就是21.

image.png

image.png

类、父类、接口索引集合

知道了我们这个是什么类,还不够,因为我们的java是支持继承的,那这个关系是怎么在类文件中表示的呢?用索引,类索引和父类索引都是用u2来表示,接口索引呢自然就是一组u2集合了。索引用于确定这个类的全限定名,Java是不支持多重继承的,所以一个类的父类索引只有一个,除了Object类,其他类都有父类,那有父类,父类的索引就不是0

那接口可以多实现啊,所以接口是用一个索引集合来表示的,这些被实现的接口按implements关键字后的接口顺序从左到右排列。类索引、父类索引、接口索引集合都按顺序排列在访问标志之后,我们可以看到接下来是0304,00,分别代表类索引,父类索引和索引接口集合。

image.png

字段表集合

字段表用于描述接口或者类中声明的变量,java语言中的字段包括类级变量和实例级变量,但不包括在方法内部声明的局部变量,字段可以包括的修饰符有字段的作用域,有没有static修饰、final修饰、并发可见性、可否序列化、字段数据类型和字段名称。

修饰符比较好搞,要么有要么没有,所以很适合搞个fiag,而字段名字、定义为什么类型,数据都是不固定的,只能引用常量池的常量描述。

字段表如下所示: image.png

字段访问标志如下所示: image.png

由于语法限制,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED都只能有一个,接口中的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志,这些都是由Java语言规则本身导致的。

跟着访问标志后面是两个参数:name_indexdescriptor_index,都是对常量池的引用,分别代表着简单名称以及字段和方法描述符,还有前面提到的类的全限定名,一锅端了。

全限定名:就是把类名的"."都替换成"/",多个连续的全限定名还会用";"分割。 简单名称:没有类型和参数修饰的方法和字段名称。 方法和字段描述符:描述字段的类型、方法的参数和返回值,根据描述符规则,基本类型以及代表无返回值的Void类型都用一个大写字符来表示,对象类型则用字符L加类的全限定名表示。

//图片改过 描述符标识字符含义: image.png

注:《java虚拟机规范》中Void类型是单独列出的VoidDescriptor,为了偷懒,我就都搞成基本类型放一起了。

那搞完了基本类型,数组类型怎么描述呢?对于数组类型呢?每个维度将使用一个前置的[,二维数组呢?就用两个[。我们常见的“java.lang.String[][]”->[[Ljava/lang/String;”一个整型数组“int[]”将被记录成“[I”。

那方法呢?描述符如何来描述方法?说答案之前,我们先猜想一下,方法有参数列表和返回值,之间肯定有顺序吧,实际上是参数列表在前,返回值在后,参数列表的参数也肯定是不变的,返回值类型就用一个字符搞定,那应该是(参数列表)+一个字符+类的全限定名,我们来看看真实情况:如方法java.lang.String toString()的描述符为:()Ljava/lang/String,还行,我们来个难一点的:

方法int indexOf(char[]source,int sourceOffset,int sourceCount,char[]target,
int targetOffset,int targetCount,int fromIndex)的描述符为:我们来推导一下,学完就得用啊,别怕,看到一个int,🆗i,接下来是参数列表:一个字符C数组[,依次类推(C[IIC[III)I,然后我们就搞完了,看看答案之前,检查一下,发现不对,数组得在前啊,又不是数组变量,搞错了,我们就重来,更正:([CII[CIII)I,对一下答案:([CII[CIII)I。

image.png

字段个数是01就是1个,访问标志前面讲了02代表私有,接下来是05代表m06i,根据访问描述符的叙述,我们可以推断出原字段是private int m;是不是很有趣,哈哈哈。

描述符之后,跟着是一个属性表集合,用于存储一些额外的信息,关于属性表我们后面详细描述,这里只提一下,需要注意的是字段表中不会出现父类或者父接口继承而来的字段,但可能会出现Java里不存在的字段,你说这不是闹鬼了嘛,并不是哈,因为我们的编译器本来就会自动添加指向外部类实例的字段,另外在java语言是不允许重名的,但是类文件中允许,只要描述符不同就行哈。

方法表

搞定了字段表,方法表也就变得很容易了,因为二者很像,几乎完全一致,结构方面也是依次包含访问标志、名称索引、描述符索引、属性表集合等几项,如下图:

方法表结构

image.png

方法表标志

image.png

那不知道大家有没有疑问,我们把方法的壳都描述的很清楚,那方法体哪里去挖?咦,我们好像一直没有说方法体的事,方法体呢是被我们的javac编译器编译成这个指令后就放在方法属性表集合里的名叫Code的属性表里面,属性表我们接下来会提到,所以大家不要担心哦。

接着往后找,意思都差不多,入口处是方法的数量2个,接着是访问标志1,对应public,接下来名称索引是7,对应常量池是初始化方法,这是编译器添加的方法,描述符索引是8,属性个数1个,对应常量池的9,看来是个Code

image.png

java语言中,要重载一个方法,除了与原方法的名称相同,还要有一个与原方法不一样的特征签名,特征签名是指一个方法中各个参数在常量池中的字段符号引用的集合,返回值不会包含在这个集合里面,所以不靠返回值区分,但Class文件区分范围的更大,返回值也是算的。

📝题外话

最后剩余的就是属性表部分了,关于属性表怕大家看的过于疲劳,所以就单独做一个章节

我是是一个简单而又平凡的人,也希望在经历过社会的残酷之后,依然简单而又平凡。

不知何时开始,我开始感觉到了生命的可贵,我每天都能看见120急救的身影,知道每时每刻都会有新的生命出现,也会有新的生命离去,人的一生漫长而又短暂,有没有想过,如果你只剩下5年时间,你选择干什么,或者我们简单一点,你打算用5年时间干什么,想听听你们的答案,看看你们是否有长远的规划,是否活一天算一天,生命这么短暂,真的甘心活一天算一天嘛?

我们来看看苦李小伙伴的答案,这是他的亲身经历,此时的他,本人已经身处二线城市,过着财富自由的生活,有时候改变真的是只需要一个人在你的睡梦中点醒你而已。 25岁那年,苦李从京东离职,跳槽百度。

在百度认识了当时的架构师久哥(T9级别),因为他的一番话,苦李从一名普通的技术工人变成别人眼中的技术大咖。

当时因为业务需要,也承蒙久哥照顾,他跟苦李说过这样一段话:

他问,“如果用5年的时间学习数据库,你能不能成为这个领域的专家?”苦李回答说,“应该可以吧”他说,“你现在25,5年后也才30,30岁就能成为某个领域的专家,为什么不去做呢?你看看周围有多少30岁的人还一事无成,而那个时候的你已经是数据库专家了。”

所以你愿意花5年时间做什么呢,说说你的答案吧?