深入理解JVM - 类文件结构
前言
JVM的类文件结构基本都会要记忆的内容,我相信你也记不住,当然我也是记不住的,所以这里只会列出大致的类文件结构,我们需要大致了解类文件结构是怎么一回事就行了,具体到那个位存哪个内容,内容确实太多了,感兴趣可以直接去读书中对应的第6章 类文件结构
这一个章节的内容。
类文件结构个人认为需要注意的点就是这几点:大致的类文件结构,部分Jdk的特性如何通过改动class文件结构实现,比如泛型,自动拆装箱,动态代理,lambada语法等。
概述:
其实主要内容就是介绍CLASS的文件结构。
- 了解JVM的类文件基本结构。
- 了解常量池的内容
- 了解重点内容属性表集合
思维导图
下面是思维导图的地址:https://www.mubucm.com/doc/1-ZGc-07kvB
什么是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种。
1 | 属性结构的内容如下: |
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的特性,可以从这个属性表开始。
写在最后
不管看几次,我也记不住,哎……