深刻了解 JVM – 类文件构造
前言
JVM 的类文件构造根本都会要记忆的内容,我置信你也记不住,当然我也是记不住的,所以这里只会列出大抵的类文件构造,咱们须要大抵理解类文件构造是怎么一回事就行了,具体到那个位存哪个内容,内容的确太多了,感兴趣能够间接去读书中对应的 第 6 章 类文件构造
这一个章节的内容。
类文件构造集体认为须要留神的点就是这几点:大抵的类文件构造,局部 Jdk 的个性如何通过改变 class 文件构造实现,比方泛型,主动拆装箱,动静代理,lambada 语法等。
概述:
其实次要内容就是介绍 CLASS 的文件构造。
- 理解 JVM 的类文件根本构造。
- 理解常量池的内容
- 理解重点内容属性表汇合
思维导图
上面是思维导图的地址:https://www.mubucm.com/doc/1-…
什么是 Class 类文件?
.class
文件是由 .java
通过 Javac
的命令编译而来的,也是 JVM 实现跨平台的要害,同时 Class 类文件实际上的内容是蕴含 字节码指令 的二进制文件,而字节码指令简略了解是 jvm 对于汇编指令的进一步封装,甚至有一些书籍拿字节码的指令来讲局部操作系统的底层逻辑实现,留神不要被洗脑了,JVM 的字节码指令只能被 JVM 辨认,放到别的平台就是一堆乱码,如果带歪了倡议看 CSAPP 这本书洗回来。
既然是由内部的.java 文件翻译并且加载到虚拟机上,那么 class 文件的构造毫无疑问须要严格的规定,避免代码毁坏 jvm 失常执行,事实上《JVM 虚拟机标准》规定了 Class 文件的整个构造,对于每一位都有严格的要求,古代的 JDK 尽管对于这个标准有了不少的改变,然而整体来看最根底的构造还是依照最初始公布的那一套执行,所以根本不须要放心过期的问题。
任意一个 class 文件对应一个类或者接口定义,class 文件构造也能够看做是一种标准,只有其余语言也能恪守 class 文件的标准,意味着齐全能够通过编写其余语言的程序转化为 class 文件最终翻译到 JVM 中。
class 文件构造
Class 文件是一组 以 8 个字节为根底 单位的 二进制流 ,各个数据我的项目 严格依照程序 紧凑地排列在文件之中,两头 没有增加任何分隔符 ,这使得整个 Class 文件中存储的内容简直全副是程序运行的必要数
据,没有空隙存在。当遇到须要占用 8 个字节以上空间的数据项时,则会依照高位在前 [2] 的形式宰割
成若干个 8 个字节进行存储。
Class 文件格式采纳一种 相似于 C 语言构造体 的伪构造来存储数据,这种伪构造中只有两种数据类型:“无符号数”和“表”。
无符号数:代表了根本的数据类型,比方 u1、u2、u3 等,数字代表了字节,比方 1 个字节,2 个字节,3 个字节,无符号数能够形容数字,索引援用或者通过 UTF- 8 的编码为字符串存储
比方
\u304
这种字符串
表 :表是由 多个无符号数 或者其余表作为数据项形成的复合数据类型,习惯性以“_info”结尾。
最初能够整个 class 文件构造看出如下的一张表。
class 文件构造详解
理解了 class 文件的大抵构造,上面来聊聊 class 文件的具体组成了。
魔数0xCAFEBABE
在 class 文件的构造中,每个 Class 文件的头 4 个字节被称为魔数(Magic Number),它惟一的作用是标记这个文件是一个 class 文件,除此之外没有其余作用,至于为什么叫做咖啡宝贝是因为它象征着驰名咖啡品牌 Peet’s Coffee 并且深受欢送的 Baristas 咖啡。
次主版本号
为什么叫做次主版本号?是因为接着魔数的前面的位数第 5、6 个字节被称为次版本号,第 7、8 个字节是主版本号。Java 的版本号是从 45 开始的,JDK 1.1 之后的每个 JDK 大版本公布主版本号向上加 1(JDK 1.0~1.1 应用了 45.0~45.3 的版本号)。高版本反对向下兼容,然而低版本不反对向上兼容,哪怕代码截然不同。《Java 虚拟机标准》
例如,JDK 1.1 能反对版本号为 45.0~45.65535 的 Class 文件,无奈执行版本号为 46.0 以上的 Class 文件,而 JDK 1.2 则能反对 45.0~46.65535 的 Class 文件。目前最新的 JDK 版本为 13,可生成的 Class 文件主版本号最大值为 57.0。
上面是书中给出的具体案例:
上面是一个 JDK 版本号对应参考图:
从 JDK 9 开始,Javac 编译器不再反对应用 -source 参数编译版本号小于 1.5 的源码
起因:JDK9 的 模块化 ,以及 扩大类加载器 的改变。
常量池
留神:常量池的入口须要搁置一个 U2(16 进制的 F)类型的数据,用于记录 常量池的容器计量值
主次版本号之后就是常量池的内容了,能够间接看做是 class 文件的资源仓库,同时是 class 文件构造关联最多的数据局部,也是最大的数据项之一。光靠这一篇文章必定是无奈讲完的,同样即便讲完了也记不住,所以这一部分咱们只须要把握寄存的内容即可。
寄存内容
常量池中次要寄存两大类常量:字面量(Literal)和 符号援用(Symbolic References)。
字面量:比方文本字符串,final 常量
符号援用则寄存如下内容:
- 模块导出或者凋谢包
- 类与接口全限定名称
- 字段与描述符
- 办法句柄和类型
- 动静调用点和动静常量
常量表
常量池的每一个常量都是一个表,最后只有 11 种构造,起初扩大出 4 种 和动静语言相干的常量,为了模块化:退出 CONSTANT_Module_info 和 CONSTANT_Package_info 两个常量,最终就是 11+4+2 = 17 种常量,所以到 JDK16 版本为止有 17 种常量。批准,记是记不住的,咱们也不须要记住:
下面的表重点关注:CONSTANT_Dynamic_info和CONSTANT_InvokeDynamic_info
上面咱们挑重点看一下这些常量对于 JDK 的影响。
Constant_class_info 类型
CONSTANT_Class_info类型,此类型的常量 代表一个类或者接口的符号援用,次要构造为一个 U1 类型的 tag(标记位)和 u2 类型的 name_index(名称援用)。
tag 的作用是标记位辨别常量的类型,而 name_index 示意常量池的索引值。
U1 代表一个字节:1111,U2 代表两个字节:11111111,也就是 65535。
后续以此类推,不再赘述。
在讲下一个类型之前,咱们先提一个问题:JAVA 办法最大长度是多少,为什么?咱们都晓得办法起名是有下限的,然而到底的下限是多少,这里咱们依据 class 的文件构造来进行解读:
CONSTANT_Utf8_info 类型
CONSTANT_Utf8_info类型常量,此常量代表了这个 类(或者接口)的全限定名。
CONSTANT_Utf8_info类型常量指向 name_index,0x00002 常量池第二项常量,标记为0x01
。接下来就是重点了,CONSTANT_Utf8_info型常量的最大长度也就是 Java 中办法、字段名的最大长度。而这里的最大长度就是 length 的最大值,既 u2 类型能表白的最大值 65535。所以 Java 程序中如果定义了超过 64KB 英文字符的变量或办法名,即便规定和全副字符都是非法的,也会 无奈编译。
书中提到的常量也是这两种,如果咱们想要查看字节码,能够应用 javap
指令。
JDK 7 时减少了前三种:CONSTANT_MethodHandle_info、CONSTANT_MethodType_info 和
CONSTANT_InvokeDynamic_info。DK 11 中又减少了第四种常量 CONSTANT_Dynamic_info
拜访标记
在常量池完结之后,用两个字节示意拜访标记与接口的访问信息。外面的具体内容包含:这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被申明为 final
access_flags(拜访标记)中一共有 16 个标记位能够应用,以后只定义了其中 9 个,如果要计算它的值,能够应用0x0001|0x0020=0x0021
。
类索引、父类索引与接口索引汇合
简略来说,Class 文件中由这三项数据来确定 该类型的继承关系 。类索引用于确定 这个类的全限定名,而父类索引中记录了以后类的父类的全限定名,须要留神的是因为 JAVA 的顶级父类永远是 java.lang.Object,除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0,而接口索引汇合就用来形容这个类实现了哪些接口。
字段表汇合
字段表(field_info)用于形容接口或者类中申明的变量,留神字段蕴含了 java 当中的类变量或者实例级常量。这里可能有一个疑难,为什么办法中的局部变量不属于字段?**。
首先,咱们须要留神的是,办法中的变量都是属于栈帧范畴内的,所以办法中的变量生命范畴逃不开一个栈帧的高度(或者说容量)。类中的字段能够定义是否公开,是否动态,援用是否不可批改等,然而局部变量做不到,所以字段表中无奈寄存局部变量,也没有必要寄存。
同样的,上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适宜应用标记位来示意。
接下来书外面的内容就是讲述字段表汇合的细节了,因为咱们不须要去钻研 GC,所以这里感兴趣能够间接去看书外面的内容。
办法表汇合
和字段表内容大致相同,Class 文件存储格局中对办法的形容与对字段的形容采纳了简直完全一致的形式,办法表汇合包含了:拜访标记(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表汇合(attributes)。同时在标记局部减少了和办法相干的标记。
既然是办法表汇合,那代码到哪去了?答案是在办法属性外面有一个 code 属性,这属性就是寄存办法中代码的中央,也是对于程序员来说最有用的中央。这里须要留神须要留神的是如果父类办法在子类中没有被重写(Override),办法表汇合中就 不会呈现来自父类的办法信息。
同样,须要留神的是尽管 java 语法不反对返回值重载然而在 class 特色签名返回值不同也能够同时存在。
须要留神的是 volatile 关键字和 transient 关键字在办法表汇合当中不一样,
属性表汇合(外围)
属性表的内容次要是搭配后面所讲的字段和办法进行搭配的,最后的预约义属性最后只有 9 种,最新的《Java 虚拟机标准》的 Java SE 12 版本中,当初曾经有了 29 种。
属性构造的内容如下:- u2 attrcibute_name_index
- U4 attribute_length
- U1 info
Code 属性
Java 程序办法体外面的代码通过 Javac 编译器解决之后,最终变为字节码指令存储在 Code 属性内。然而须要留神并不是所有的办法都要有这个属性,因为办法的内容是 能够为空 的。
Code 属性是 Class 文件中最重要的一个属性,如果把一个 Java 程序中的信息分为代码(Code,办法
体外面的 Java 代码)和元数据(Metadata,包含类、字段、办法定义及其他信息)两局部,那么在整
个 Class 文件里,Code 属性用于形容代码,所有的其余数据我的项目都用于形容元数据。
后续的内容是依据的一个 javap 生产的字节码指令来进行相干的解读,这些内容也是不是要害的内容,为了加重记忆负担,本文也不做过多介绍。
异样表
Code 数量外面还蕴含一个异样表,异样表也是 JAVA 代码的一部分,这部分内容尽管能够通过 GOTO 这种跳转指令实现,然而在 JVM 标准中是强制标准 JAVA 语言应用异样表而不是 GOTO 指令实现 JAVA 的异样以及 Finally 的解决机制。
为什么不能用 GOTO?这就要问问 C 语言这个老先生了,尽管很多语言都保留了 GOTO 的语法,然而无一例外没有人举荐应用。因为它不仅容易出 BUG,并且写进去的源代码非常难以了解。
line_number_table 属性
- 用于形容行号和字节码行号对应关系
- 用于 debug 应用
-
-line_number_info 中两个 u2 类型的数据项
- start_pc: 字节码行号
- line_number java 源代码行号
因为篇幅问题,其余的属性这里就不过多介绍了,思维导图摘录了大抵内容,在遇到疑难的时候翻一翻记忆会比拟深,这里也不做过多叙述。
总结
class 文件构造靠着死记硬背是记不住的,须要依据 JVM 的个性来进行了解,比方动静语言是如何实现的,再比方 DEBUG 是如何实现的等等,用这些内容帮忙了解记忆。
从本文也能够看到,其实重点都在 属性表汇合 这一部分。所以如果须要重点了解 JAVA 的个性,能够从这个属性表开始。
写在最初
不论看几次,我也记不住,哎 ……