马上过年啦,不知道大家今年有没有投资基金股票呢?是赚的盆满钵满还是拍断大腿,可以评论区一起交流交流,秀一秀哈哈,反正我是没来得及上车。
暴富西不可能暴富的啦,打工人嘛几能写写文章啦~记得点赞➕关注呀
上节回顾
1.创建对象的7个步骤
2.java对象的内存布局了解多少
3.内存分配的并发问题怎么解决
一点说不上来的同学注意啊!你离暴富又远了一步,好在我这儿有本秘籍,暴富秘籍
写在前面
从本章开始,我打算用2章的篇幅来讲述一个类文件结构和类的加载过程。很多工作了几年的同学都会有这种感受:几个月不学技术,不去接触新的东西,渐渐的就会跟不上潮流了。就像前几年一直鼓吹中台,到现在又要拆中台,这都是种种原因所导致的,可理解为是一种潮流。但不管潮流怎么变,最底层的知识永远都不会轻易动摇和变化的,我们现在学的可能未来5年、10年都不会有所改变,对于短短的编码生涯来说基本属于终生受益了。九九乘法表虽然枯燥难背,但现在不也深入每个人的生活,并终生受用。
本章内容
本章我们主要学习class类文件的结构,要想更好的理解和掌握本章的知识点的话,需要同学们也上手一起来操作,同时针对一些常用的字节码指令简单进行一些介绍。
一、Class类文件的结构
Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文
件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数
据,没有空隙存在。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前的方式分割
成若干个8个字节进行存储。
Class文件有两种数据结构:无符号数和表
无符号数:属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个
字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串
值。
表:是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名
都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视
作是一张表。
在解析之前我们需要下载一个能查看class文件内容的工具,我这边使用的是Hex Fiend,windows系统的同学也可以下载WinHex都是一样的。
希望同学们也能自己下载一个也尝试着来解析一下,这样就不至于隔天就忘记了。
使用javac指令生成对应的class文件,并把文件拖入Hex Fiend中打开
好了,今天我们要将的就是这些个16进制数字组合。
1、魔数
每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为
一个能被虚拟机接受的Class文件。
java使用的魔数是CAFEBABE,这个压根不用记住,看一眼java图标就显而易见了
以前一直不清楚这图标到底是咖啡还是茶呢,本人喝茶比较多一点,当然老外的话一定是咖啡比较多。现在相信大家都能确定这就是一杯咖啡。
2、文件版本号
紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor
Version),第7和第8个字节是主版本号(Major Version)。
至于版本号的作用相信大家都能想到,毕竟java从JDK1.1开始,到现在JDK16新特性都已经出来了,大版本都已经有十几个了,这还是不计算各个小版本的情况下。为了保持非常良好的向后兼容性,这个版本号必不可少,所谓向后兼容就是说低版本的JDK下开发的产品能在高版本的JDK中跑。
Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1(JDK 1.0~1.1使用了45.0~45.3的版本号)。这边我们看到主版本号的16进制是34,对应的10进制就是52,也就是说我们从这个就能推断出,我们是用JDK1.8来编译生成这个class文件的。
关于次版本号,曾经在现代Java(即Java 2)出现前被短暂使用过,JDK 1.0.2支持的版本45.0~
45.3(包括45.0~45.3)。JDK 1.1支持版本45.0~45.65535,从JDK 1.2以后,直到JDK 12之前次版本号均未使用,全部固定为零。而到了JDK 12时期,由于JDK提供的功能集已经非常庞大,有一些复杂的新特性需要以“公测”的形式放出,所以设计者重新启用了副版本号,将它用于标识“技术预览版”功能特性的支持。如果Class文件中使用了该版本JDK尚未列入正式特性清单中的预览功能,则必须把次版本号标识为65535,以便Java虚拟机在加载类文件时能够区分出来。
3、常量池
同学们应该都还记得在DAY1中我们曾经讲过方法区中的运行时常量池,我们这边讲的常量池指的是class文件中的常量池,这个大家要区分开。两者的区别就是运行时常量池具备动态性,这里我们就不展开讲了。
紧接着主、次版本号之后的是常量池入口,由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。
这里我们看到这个16进制值是0013,对应的10进制就是19。与Java中语言习惯不同,这个容量计数是从1而不是0开始的,所以记录的是#1到#18的常量。
这个可能不大好理解。我们通过事实来证明一下,使用javap -c -v指令来看一下我们的class文件,眼见为实
显而易见这里一共是18个常量。
对应到我们Hex Fiend里面的字段的话就是这大段
大家先别慌,这些其实都是死的,只要参照着字典表一个个对照来查找就很简单了,下面我们也一起来走一遍。
常量池解析实战
首先拿出我们的字典表,先放着不急着看
每一种类型都是以_info结尾的,证明这是几个常量都是表,笼统地去看不难发现,每一个常量的开头都是一个u1字节长度的tag,这就好比是一个id号,大家可以这样来理解。
1.第一个常量
看图中,紧接着常量池入口的第一个字节就是我们第一个常量的tag,他的值为0A,也就是10,根据这个id查找字典,找到
也就是说这个常量是一个u1和两个u2组成的,一共5个字节,很快哦,啪~一下我们就把第一个常量找到了
其他的都是类似的,除了tag = 01的utf_8比较特殊
2.常量CONSTANT_Utf8_info
这个常量的第二个项目的含义是lenth也就是第三个项目有多少个位数的含义。
我们用第一个CONSTANT_Utf8_info常量来作为例子
tag = 01表明他是一个CONSTANT_Utf8_info常量,0001表示这个常量的长度是1,6D就是长度为1的UTF-8编码的字符串
这样分开来讲应该很容易理解来把。同学们能自己把这18个常量一一找出来吗?
评论区找答案哦~
4.访问标志
在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或
者接口层次的访问信息。
可以看到这边的16进制值是0x0021
access_flags中一共有16个标志位可以使用,当前只定义了其中9个,没有使用到的标志位要求一
律为零。
每一个访问标志占用一位目前一共又9位,还有6位还未使用,全部记为0。
下面我们看一下这个访问标志是怎么算出来的
首先把0x0021转换为2进制
0000000000100001
也就是说我们第0位和第5位为1的两个访问标志位为1,其他全部为0,对应到上面表格中很容易就能查出,这边除了ACC_PUBLIC和ACC_SUPER为真,其他全部为假。
最后我们通过javap -c -v指令来验证一下
5.类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合
(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类型的继承关系。
java是不允许多重继承,但是允许实现多个接口,所以除了Object类的父类索引是0x0000,其他类的父类索引都不为0,父类索引后面紧跟着的是一个u2长度的接口索引集合计数,可以类比常量池计数器,接口索引集合按照代码中接口实现的顺序从左到右记录索引位置。
对应到我们的Hex Fiend工具里面是这3个u2字符,分别表示
类索引: 0x0003 代表常量池的#3号常量
父类索引:0x0004 代表常量池的#4号常量
接口索引集合计数器: 0x0000 代表没有实现任何接口
考虑到本章讲的内容相对来说比较多,而且看到到这里跟着我一起一边学一边操作的同学应该已经对类文件的结构的解析套路有了一个大致的了解了,所以正文的类文件结构就告一段落。剩余的字段表集合、方法表集合和属性表集合这三个部分我另外开一章来写,感兴趣的同学可以继续进入进阶学习
最后附上常量池解析的结果
全部正确的同学恭喜你已经小有所成啦,点赞👍+关注,下一篇的主题是类加载~