大学时有个段子:什么是最牛逼的程序猿?

最牛程序猿深情地注视着屏幕上一堆16进制,露出满足而诡魅的微笑。

同学问他在看啥,他小声说:蒼井そら。

image-20201203084643095

好,下面,我们也来学习阅读一下16进制版的Class文件。

1.源头:Class规范

每一个Java版本发布时,会公布两份规范文档(以Java8为例):

Java语言规范:《The Java Language Specification, Java SE 8 Edition》

Java虚拟机规范:《The Java Virtual Machine Specification, Java SE 8 Edition》

Java虚拟机规范中有很大篇幅就是描述Class文件格式的规格,它也是帮助我们学习、理解Class文件最好的参考资料。

image-20201203090336783

2.验证:演练代码

为了更好地覆盖JVM规范中描述的Class文件格式主脉络,笔者设计了如下示例代码:

image-20201203085719069

这段代码体现了5种Java的语法特征:

类:Demo2

成员变量:私有成员变量filed1,int类型,初始值为1

方法:公有方法hello,无参数无返回值

局部变量:局部变量i,int类型,初始值为1

语句:打印语句println,打印内容为hello world

使用javac编译上述代码,我们会得到Demo2.class文件,用16进制编辑器打开如下:

image-20201203101943544

为了更清晰的查看,我们将上述数据复制到Excel表格中,着色

image-20201203112942827

3.合法性检查依据:魔数、版本号

首先,通过阅读规范,我们获得如下知识:

魔数:4个字节

副版本号:2个字节

主版本号:2个字节

image-20201203102514844

然后,在IDE中将魔数标记出来:

image-20201203113140375

然后,在IDE中将版本号标记出来:

image-20201203113240887

  • 分析

魔数值和版本号是JVM加载Class文件时的合法性检查依据:

如果16进制文件内容的开始不是CA FE BA BE,则JVM就认为这个文件不是合法的Class文件;

如果16进制文件内容的版本号与JVM版本号不匹配,则JVM就拒绝加载。00 00 00 34,表示Java8对应的JVM可以加载。

  • 技术直男的浪漫

这里值得提到一个趣闻:魔数值CA FE BA BE,应该这样断句:CAFE BABE

Gosling,这位资深技术直男,就是这样温情地形容,他发明的这个编程语言——咖啡宝贝

image-20201203105231554

4.信息仓库:常量池

4.1.常量池计数器

在笔者前一篇文章《【编译引擎】学习阅读Class文件结构的意义》中提到:

常量池:本质是一张表格。

常量:是站在JVM角度看,公用的一些常量值,为了压缩Class文件的大小而存在。

因此,JVM规范才规定Class文件中,有2个字节的常量池计数器(表示常量表格的长度),紧接着的N个字节存储M个常量的具体内容

M个常量:M=常量池计数器的值 - 1

N个字节:N是变长的,因为每个常量类型不同,有的常量需要4个字节存储,有的常量需要很多字节存储。

image-20201203110434464

  • 首先,找到常量池计数器=00 24,转换为10进制是36,说明常量池中存储了35个常量

一个规范中的细节:The constant_pool table is indexed from 1 to constant_pool_count - 1.

这段话的意思是,常量池计数器值-1就是常量个数

image-20201203114851834

4.2.N种常量类型

接下来,我们解读具体的常量,前文讲过,常量有不同的类型,那么Class文件如何表达常量类型呢?

规范中,采用了如下结构表示不同类型的常量:

image-20201203115414837

那么,JVM支持多少种类型的常量呢?

image-20201203115520340

本文的演练代码中,涉及到了种类型的常量:

CONSTANT_Utf8:1

CONSTANT_String:8

CONSTANT_Class:7

CONSTANT_Fieldref:9

CONSTANT_NameAndType:12

CONSTANT_Methodref:10

4.3.CONSTANT_Utf8类型的常量

JVM规范:The CONSTANT_Utf8_info structure is used to represent constant string values

这种类型的常量,就是表达字符串字面量。

它的结构如下:

image-20201203120726026

我们以第25个常量为例,尝试理解一下:

image-20201203120926294

这个常量值=01 00 0B 68 65 6C 6C 6F 20 77 6F 72 6C 64,如下图所示的含义

image-20201203121756603

第25个常量,表示的就是演练代码中的打印的字符串内容:

image-20201203122016513

4.4.CONSTANT_String类型的常量

JVM规范:The CONSTANT_String_info structure is used to represent constant objects of the type String:

这种类型的常量,是字符串对象类型的,它的值指向4.3中描述的字符串字面量。

它的结构如下:

image-20201203122939616

我们以第5个常量为例,尝试理解一下:

image-20201203122644594

这个常量的值= 08 00 19,如下图所示的含义:

image-20201203123306833

结合第5个常量和第25个常量,再看演练代码,我们窥见JVM处理字符串的原理

image-20201203123552479

4.5.CONSTANT_Class类型的常量

JVM规范:The CONSTANT_Class_info structure is used to represent a class or an interface

这种类型的常量,表示类或者接口

它的结构如下:

image-20201203124237102

我们以第6个常量为例,尝试理解一下:

image-20201203124334512

这个常量的值= 07 00 1C,结合第28个常量,如下图所示的含义:

image-20201203124818815

再看演练代码,CONSTANT_Class类型的常量表达了演练代码中定义的Demo2类:

image-20201203124958357

4.6.CONSTANT_Fieldref类型的常量

JVM规范:Fields, methods, and interface methods are represented by similar structures:

这种类型的常量,表示字段(例如:一个类中的成员变量)。JVM规范还告诉我们,实例方法、接口方法都是同样的Class格式。

image-20201203125455813

我们以第2个常量为例,尝试理解一下:

image-20201203125918470

这个常量的值= 09 00 06 00 16,结合第6个常量和第22个常量,如下图所示的含义:

image-20201203130857438

但是第22个常量=0C 00 08 00 09表示了什么呢?我们看下一章节的解读

4.7.CONSTANT_NameAndType类型的常量

JVM规范:The CONSTANT_NameAndType_info structure is used to represent a field or method, without indicating which class or interface type it belongs to

这种类型的常量,表示一个字段的类型信息

image-20201203131307059

我们以第22个常量为例,尝试理解一下:

image-20201203131134360

这个常量的值= 0C 00 08 00 09,结合第8个常量和第9个常量,如下图所示的含义:

image-20201203131856257

至此,我们就看到了演练代码中在JVM中如何表达int field1信息了:

image-20201203132039284

4.8.CONSTANT_Methodref类型的常量

JVM规范:Fields, methods, and interface methods are represented by similar structures:

这种类型的常量,表示方法。JVM规范还告诉我们,它的结构和字段类型常量都是同样的Class格式。

image-20201203132452923

我们以第5个常量为例,尝试理解一下:

image-20201203132633860

这个常量的值= 0A 00 1A 00 1B,结合第26、27个常量,如下图所示的含义:

image-20201203134131024

5.总结:常量池构建的静态全局观

根据演练代码产生的Class文件,JVM构建了35个常量:

image-20201203134423990

我们以图形化的方式进一步看看,Class文件的常量池到底为JVM构建了怎样的信息树

5.1.源代码中有哪些方法引用

常量池构建了方法引用的信息树,我们可以看到两颗方法引用树,如下图:

image-20201203143257328

以其中一棵树为例,遍历这棵树,我们可以得到如图的信息:

源代码中有一个方法,此方法属于Object类,方法名为,方法无输入参数,无返回

image-20201203143352283

5.2.源代码中有哪些字段引用

常量池构建了字段引用的信息树,我们可以看到两颗字段引用树,如下图:

image-20201203143527781

以其中一棵树为例,遍历这棵树,我们可以得到如图的信息:

源代码中有一个字段,此字段属于Demo2类,字段名为field1,字段数据类型为int

image-20201203143719972

5.3.源代码中有哪些字符串对象

常量池构建了字符串类型对象的信息树,我们可以看到一颗字符串对象树,如下图:

image-20201203143839084

以这棵树为例,遍历这棵树,我们可以得到如图的信息:

源代码中有一个字符串对象,此字符串对象的值是hello world

image-20201203143944970

5.4.源代码中其它的字符串字面量

image-20201203144014111

5.5.静态的全局观

至此,我们已经看到了常量池为JVM诠释了Java源代码中的信息树。

常量池中的这些信息树,为JVM构建出了一个静态的全局观

通过这些信息树,我们已经能反演出Java源代码中的静态结构

有几个类?

类中有几个成员变量?

类中有几个方法?

每个成员变量的名称、数据类型

每个方法的名称、输入参数列表、返回值类型

但常量池中还有一些信息孤点,我们无法反演出一些动态结构

成员变量的状态变化过程

方法体内的运行过程

方法内局部变量的变化过程

6.下一步

笔者下篇会继续阅读本演练代码对应的Class文件剩余部分,包括

访问标识

类索引

字段表

方法表

属性表

进一步理解Class字节码如何为JVM构建动态全局观

**高能预警:**如果您能耐心地看到这句话,恭喜您,距离”看着16进制享受而诡魅的微笑“又近了一步,加油!

image-20201203144947702