第六章 类文件结构
6.1 概述
略
6.2 无关性的基石
因为想要实现 “Write Once,Run Anywhere”的伟大理想,Java 虚拟机被发明了出来。这些虚拟机都可以载入和执行同一种平台无关的程序存储格式——字节码(ByteCode),这就是构成无关性的基石。有的文章中只说明了平台无关性,我认为这也同样是语言无关性的基石。
平台无关性已是大家所熟知的,它指的是不论是在 Windows 平台还是在 Linux 平台上,都可以通过载入字节码(也就是我们常说的 .class 文件)执行。而所谓语言无关性则是指的 Java 虚拟机不和包括 Java 在内的任何语言绑定,它只与 “Class 文件” 这种特定的二进制文件格式所关联。任一功能性语言都可以表示为一个能被 Java 虚拟机所接受的有效的 Class 文件,也就是说任何其他语言的实现者都可以讲 Java 虚拟机作为语言的产品交付媒介。
6.3 Class 类文件的结构
Class 文件是一组以 8 字节为基础单位的二进制流,各个数据项目都严格按照顺序紧凑的排列在 Class 文件中,中间没有任何分隔符,这使得整个 Class 文件中存储的内容几乎全是程序运行的必要数据,中间没有空隙存在。当需要占用8字节以上空间的数据项时,则会按照高位在前的方式分割成若干个 8 位字节进行存储。引入 Java 虚拟机规范的话:
每个 Class 文件都是由 8 字节为单位的字节流组成,所有的 16 位、32 位和 64 位长度的数据将被构造成 2 个、4 个和 8 个 8 字节单位来表示。多字节数据项总是按照 Big-Endian 的顺序进行存储。在 Java SDK 中,访问这种格式的数据可以使用 java.io.DataInput、java.io.DataOutput 等接口和 java.io.DataInputStream 和
java.io.DataOutputStream 等类来实现。
根据 Java 虚拟机规范的规定,Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储数据,这种结构中只有两种数据类型:无符号数 和 表。此处简要介绍下名词:
- 无符号数:无符号数属于基本类型,以 u1 、u2 、u4 、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值。
- 表:表示由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性的以“ _info ”结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件实际上本质就是一张表。“表结构”如下:
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count-1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
我们写个方法来简单验证下(看看 JDK 的 major_version 能不能对上),代码如下:
public class DecompileDemo {
private int m;
public int inc(){
return m + 1;
}
}
使用十六进制编辑器打开编译好的 DecompileDemo.class 文件,如图:
我们知道,一个字节是 8 bit,也就是 8 位二进制,一个 16 进制位是 4 个二进制位,所以一个字节是 2 个十六进制位。那么我们就验证下:
前 8 位是 magic,也就是 cafe babe,再 4 位是 minor_version 为 0000。再 4 位就是我们关注的 major_version 也就是 0034,转换成十进制 0034 就是 52。跟下表比对,对上了!
发行版本 | Major Version |
---|---|
Java SE 13 | 57 |
Java SE 12 | 56 |
Java SE 11 | 55 |
Java SE 10 | 54 |
Java SE 9 | 53 |
Java SE 8 | 52 |
Java SE 7 | 51 |
Java SE 6.0 | 50 |
Java SE 5.0 | 49 |
JDK 1.4 | 48 |
JDK 1.3 | 47 |
JDK 1.2 | 46 |
JDK 1.1 | 45 |
那么怀揣着这份激动地小心情,我们继续看下去
6.3.1 魔数与 Class 文件的版本
所谓的魔数应该就是指前表中的 magic ,也就是我们看到的 cafe babe。每个 Class 文件的头四个字节是一致的,它的唯一作用就是确定这个文件是否是一个能被虚拟机接受的 Class 文件。虚拟机识别到头四个字节是“咖啡宝贝”,那么就说明这是个 Class 文件。使用魔数而不是使用扩展名的原因是基于安全性考虑的,扩展名可以随意被改变而魔数很难。
紧接着后四位就是 Class 文件的版本号,也就是我们刚找到的 0000 0034,第5和第6个字节是次版本号(Minor Version),也就是这个 0000 ,第7和第8个字节是主版本号(Major Version),也就是 0034 。Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1。高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件(例如 52 版本的虚拟机可以运行 51 版本的 Class 文件,反过来 51 的虚拟机不能执行 52 的 Class 文件),即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的 Class 文件。
6.3.2 常量池
根据表上来说,版本号后面就是常量池。还记得常量池是啥不,它是方法区(jdk 8 以前在永久代,以后在元空间)的一部分,专门存类信息。其实在学习这里时我有些疑惑,这里的常量池和在讲方法区时提到的常量池有些概念上的不清不楚。看了一篇博客后我恍然大悟(原文戳此):
在JVM中,使用了OOP-KLASS模型来表示java对象,即:
- jvm在加载class时,创建instanceKlass,表示其元数据,包括常量池、字段、方法等,存放在方法区;instanceKlass是jvm中的数据结构;
- 在new一个对象时,jvm创建instanceOopDesc,来表示这个对象,存放在堆区,其引用,存放在栈区;它用来表示对象的实例信息,看起来像个指针实际上是藏在指针里的对象;instanceOopDesc对应java中的对象实例;
- HotSpot并不把instanceKlass暴露给Java,而会另外创建对应的instanceOopDesc来表示java.lang.Class对象,并将后者称为前者的“Java镜像”,klass持有指向oop引用(_java_mirror便是该instanceKlass对Class对象的引用);
- 要注意,new操作返回的instanceOopDesc类型指针指向instanceKlass,而instanceKlass指向了对应的类型的Class实例的instanceOopDesc;有点绕,简单说,就是Person实例——>Person的instanceKlass——>Person的Class。
再结合 周志明 翻译的 Java 虚拟机规范(Java SE 7 )中关于方法区的描述:
在 Java 虚拟机中,方法区(Method Area)是可供各条线程共享的运行时内存区域。方法区与传统语言中的编译代码储存区(Storage Area Of Compiled Code)或者操作系统进程的正文段(Text Segment)的作用非常类似,它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。
就很明白了,方法区内存的是一个个的instanceKlass,这个常量池实际上可以理解为 instanceKlass 的一个属性。思前想后原来是把运行时常量池和常量池有些混淆:
- 运行时常量池是方法区的一部分,是一块内存区域。除了常量池中写好的常量会在类加载的时候被置入,也可以在运行的时候被置入,例如 String.intern() 方法
- 常量池是 instanceKlass 的一个“属性”,在类加载的时候,常量池里的东西会被加载到运行时常量池中。
我们知道这个常量区不是固定不变的,每个类都不一样的,所以我们只能提前定义一个数据记录下常量池的大小,这就是 constant_pool_count 。为了特殊情况,此处设计时特意将 ‘0’ 索引空出来,所有的常量索引是从 1 开始的。例如常量池的长度是 5,则实际上只有 1、2、3、4 四个元素。
复习下,常量池主要存放两大类常量:
- 字面量:指 Java 层面的常量、静态变量、文本字符串、final变量等
- 符号引用:编译器层面的 类和接口的全限定名、字段描述符及名称、方法描述符及名称
由于虚拟机只有在加载Class文件时才会进行符号与内存间的动态连接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法被虚拟机直接使用。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址中,同时放入运行时常量池中。此处针对符号引用引入 @lzcWHUT 博主的解释(原文戳此):
符号引用与直接引用的关联
- 符号引用是一组符号,用来描述所引用的目标,符号是以任何形式存在的字面量。对于符号引用Java虚拟机并没有严格的限制。规定只需要使用的时候能够无歧义定位到目标就可以。常量池存在于Class文件中,而Class文件是必须首先通过Java虚拟机的类加载机制加载到内存中(确切的说是方法区这个内存区域,回顾一下,方法区存放的主要是
对象的实例类的信息,这个Class文件是虚拟机对外接受访问的接口)。符号引用属于常量池中的内容,并不是说符号引用的目标已经加载到内存中了,因为符号引用与虚拟机的内存布局无关,符号引用的目标并不一定已经加载到内存中了。- 直接引用可以是直接指向引用目标的指针、相对偏移量或者是一个能够间接定位到目标的句柄。直接引用是和虚拟机的内存布局有关的,同一个符号引用在不同的虚拟机上翻译的直接引用一般是不同的。如果有了直接引用,那么引用的目标必定是存在内存中的。
常量池中每一项通常都是一个表,这 14 个表都有一个特点:它们都以一个表示表类型的单字节 tag 项开头。具体的 tag 类型如下表所示:
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_Utf8 | 1 | UTF-8 的字符串 |
CONSTANT_Integer | 3 | 整形字面量 |
CONSTANT_Float | 4 | 浮点型字面量 |
CONSTANT_Long | 5 | 长整型字面量 |
CONSTANT_Double | 6 | 双精度浮点型字面量 |
CONSTANT_Class | 7 | 类或符号接口的符号引用 |
CONSTANT_String | 8 | 字符串类型字面量 |
CONSTANT_Fieldref | 9 | 字段的符号引用 |
CONSTANT_Methodref | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandle | 15 | 表示方法句柄 |
CONSTANT_MethodType | 16 | 标识方法类型 |
CONSTANT_InvokeDynamic | 18 | 表示一个动态方法调用点 |
这14种表每个都有自己的结构。具体十四种结构在本栏文章《 深入理解Java虚拟机-附件1 常量池中的 14 种常量项的结构总表》中有所列出。
我们继续看刚才截图的字节码(此处建议打开附件链接比对查看,帮助理解),
根据表中信息,在 0034 后也就是 0016 ,便是常量池的长度,这里换算成10进制来说就是 22 ,也就是说有 21 个常量在常量池中。下面我们一个一个分析,第一个常量的 tag 为 0a ,也就是说应该是 10 - 类中方法的符号引用。我们接着比对 CONSTANT_Methodref_info 表,除 tag 外它的第一个字段是指向声明方法的类描述符的索引项,占用了两个字节为0004。那么也就是在常量池中第四个变量就是这个类描述符。我们暂且不管,继续分析下个字段指向名称及类型描述符的索引项,同样占用两个字节为 0012。也就是说第 18 个常量是方法名称和描述符。这第一个变量分析完毕,我们来看第二个变量。tag 为 09 - 字段的符号引用,我们继续去查表 CONSTANT_Fieldref_info 。除 tag 外第一个字段是指向声明字段的类或者接口描述符的索引项,占用两个字节为 0003 ,也就是常量池中第三个变量。第二个字段是指向字段描述符的索引项,同样占用两个字节为 0013,也就是说第 19 个常量。我们再来分析表中第三个常量也就是第二个常量中第二个字段所指的常量,tag 为 07 - 类或接口的符号引用,老规矩查表:CONSTANT_Class_info,除 tag 外第一个字段是指向全限定名常量项的索引,占用两个字节为 0014 即常量池中第 20 个 变量。我们再分析第四个,tag 仍然是 07 - 类或接口的符号引用,除 tag 外第一个字段为 0015 即 第 21 个变量。
分析分析到这里,不知道你们有没有完全跟下来,我自己分析是贼混乱的。那么针对这么讨厌的工作,我们伟大的程序员怎么可能没有工具帮助呢。jdk 自带的 javap -verbose 就是干这个的。他是专门分析 Class 文件字节码的工具,让我们看下结果:
让我们来校验一下刚才分析的结果吧。
前四位代表魔数,再四位是 Minor 版本为 0,再4位是 Major 版本为 52 。这里没有什么疑问都是ok的。关键的信息是下面打印出来的会不会和我分析的一样呢,我们拭目以待:
- 常量池长度 21 ✔ \color{red}{✔} ✔
- 第一个变量是 Methodref ,第一个字段是第 4 个常量,第二个字段是第 18 个常量 ✔ \color{red}{✔} ✔
- 第二个变量是 Fieldref,第一个字段是第 3 个变量,第二个字段是第 19 个变量 ✔ \color{red}{✔} ✔
- 第三个变量是 Class,字段值为第 20 个变量 ✔ \color{red}{✔} ✔
- 第四个变量是 Class,字段值为第 21 个变量 ✔ \color{red}{✔} ✔
全对全对,哈哈哈哈。可能你们没办法体会我现在激动的心情,我真的是顺着写,顺着分析,然后再用工具反编译,没有回去改任何东西,全对!可能有人觉得不至于,毕竟只要认真的一个个比着表对就不会有错,但是正是种小小的成就感和满足感促使我进步!
言归正传,其实反编译出来的代码中有许多我们不认识的也没用过的常量,例如 m、I、()V等。这些自动生成的常量的确没有在 Java 代码里面直接出现过,但他们会被后面即将讲到的字段表、方法表等用到。他们会用来描述一些不方便使用“固定字节”进行表达的内容。譬如描述方法的返回值是什么,有几个参数,每个参数的类型是什么等。
6.3.3 访问标志
在常量池结束后,紧接着的两个字节代表着访问标志。这个标志用以标志类的一些基本信息,比如他是不是接口,是否是 public 类型,是不是 abstract 类型,是不是被声明为 final 等。具体见下表。
经过我缜密的计算,数出本次实验类的字节码中的 access_flag 应为 0021 。
那么就应该是 0x0020 | 0x0001 = 0x0021。 也就是说我们的类是 ACC_PUBLIC 和 ACC_SUPER 。截图也同样验证了我的想法:
6.3.4 类索引、父类索引与接口索引集合
按表中的顺序,接下来就是四个类型为 u2 的数据 this_class、super_class、interfaces_count、interfaces,除 interfaces 外其余的长度都是 1 。四个字段分别代表了 本类索引(本类的全限定名)、父类索引(父类的全限定名)、类接口计数器(因为可以实现多个接口,所以这里跟常量池一样,先有一个数量的字段,再是索引集合)、接口索引集合。也就是图中的 0003 0004 0000:
这里可能有人会问,不是 4 个 u2 数据吗,怎么才 12 位,应该至少 16 位才对啊。这里要讲述下,当类没有实现任何接口时,类接口计数器为0,后面接口的索引集合将不占用任何字节。我们根据前文所得数据可以得到,本类是常量池中 第三个常量,父类是第四个。根据 javap 得出的代码验证一下,果不其然是正确的。
6.3.5 字段表集合 / 属性集合(简)
字段表(field_info)是用于描述接口或者类中声明的变量。不过这只是指类级别/实例级别的变量,方法中的变量存在虚拟机栈帧中的局部变量表里,不存这里。我们跟着作者的思路想一下,一个变量都有哪些东西。 访问修饰符(public、private、protected等)、是否是静态变量(static)、是否是可变变量(final)、是否是并发可见的(volatile)、可否被序列化(transient,用transient关键字标记的成员变量不参与序列化过程)、是什么数据类型的(数组还是对象还是基本类型)、对象名叫什么。我们会发现一共分为两种,一类是修饰符类,这种都是 有 或 无类型即 Boolean 类型的。第二类就是常量池中的数据,比如对象名、数据类型。先上表吧:
类型 | 名称 | 含义 | 数量 |
---|---|---|---|
u2 | access_flags | 访问标志 | 1 |
u2 | name_index | 字段的简单名称 | 1 |
u2 | descriptor_index | 字段的描述符 | 1 |
u2 | attributes_count | 字段属性(例如 Deprecated)的长度 | 1 |
attribute_info | attributes | 属性具体值 | attributes_count |
大家一定发现里面有个 attribute_info 类型的数据,这就是属性表集合。在 Class 文件、字段表、方法表中,都可以携带自己的属性表集合以用于描述某些场景专有的信息。属性表集合针对顺序的限制稍微宽松了一些,不再要求各个属性表具有严格的顺序。只要不与已有属性重名即可。为了能正确的解析, Java 虚拟机规范(Java SE 7)中预定了 21 项属性。为防止版面过长,具体 21 项属性在本栏文章《 深入理解Java虚拟机-附件2 虚拟机规范预定义的属性》一文中有所列出。
对于每个属性,它的名称都要从常量池中引用一个 CONSTANT_Utf8_info 类型的常量来表示,属性结构则完全可以自定义的,只需要有一个 u4 类型的长度属性去表示占用位数即可,也就是需要满足下表所示结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
有了这些基础知识做铺垫,我们来实操下~
由于我们之前的例子无法展现字段方面的东西,我就稍作修改:
public class DecompileDemo {
private static final transient int mmmmmmmmmmmmm = 0;
public static int inc() throws NullPointerException{
return mmmmmmmmmmmmm + 1;
}
}
是的,加了一个简单的 @Deprecated 过期属性和一些字段修饰符。先上常量池:
下面我们来看下编译后的字节码这部分是什么样子的:
根据常量池我们知道 0002 0003 这两个是 this_class 和 super_class ,0000 是 interfaces_count。 那么 0001 就是我们要找的 feilds_count,也就是只有一个字段。下一个字段是字段表中的 u2 类型的 access_flags 字段也就是 009a ,这个东西其实跟类的 access_flags 是非常相似的,同样有个表格:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 字段是否是 public |
ACC_PRIVATE | 0x0002 | 字段是否是 private |
ACC_PROTECTED | 0x0004 | 字段是否是 proctected |
ACC_STATIC | 0x0008 | 字段是否是 static |
ACC_FINAL | 0x0010 | 字段是否是 final |
ACC_VOLATILE | 0x0040 | 字段是否是 volatile |
ACC_TRANSIENT | 0x0080 | 字段是否是 transient |
ACC_SYNTHETIC | 0x1000 | 字段是否是 synthetic |
ACC_ENUM | 0x4000 | 字段是否是枚举类 |
那么这个009a,我的算法是 2 + 8 = a, 1 + 8 = 9。所以很明显是 0x0002 | 0x0008 | 0x0010 | 0x0080。也就是说 private static final transient 。这与我们的源码是对的上的。再接着往下看,下一个是 name_index ,也就是 0004 。那么我们看常量池中第四个变量是什么,是 mmmmmmmmmmmmm ,也对上了。再接着看呢,是 descriptor_index 也就是 0005 。常量池中第五个常量是 I,那这个 I 是什么意思呢?我们之前也提到过说常量池中有许多我们不认识的字母出现,这里实际上是 8 大基本类型以及代表无返回值的 void 和 对象类型 这 10 个类型的缩写。如表所示:
标识字符 | 含义 | 标识字符 | 含义 |
---|---|---|---|
B | 基本类型 byte | J | 基本类型 long |
C | 基本类型 char | S | 基本类型 short |
D | 基本类型 double | Z | 基本类型 boolean |
F | 基本类型 float | V | 基本类型 void |
I | 基本类型 int | L | 对象类型,如 Ljava/lang/Object |
对于数组类型,每一维度将使用一个前置的 “[” 字符来描述,如一个定义为 “String[][]”类型的二维数组,将被记录为 “[[Ljava/lang/String”
用描述符来描述方法时,按照先参数列表后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号中。如 void inc() 的描述符为 “()V” ,方法java.lang.String toString() 的描述符为 “()Ljava/lang/String”,方法 int indexOf(char[]source,int sourceOffset,int sourceCount,char[]target,int targetOffset,int targetCount,int fromIndex) 的描述符为 “([CII[CIII)I”。
那么前文提到的 descriptor_index 也就是 0005 ,即 I 代表的是 int 类型。
再往下看是 u2 类型的 attributes_count 字段,也就是 0001 ,这代表着这个字段有 1 个属性。分别是什么呢?
他的 attribute_name_index 为 0006 ,也就是属性名称是常量池中第 6 个常量 - ConstantValue。ConstantValue 的属性结构如下表所示:
类型 | 名称 | 长度 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | constantvalue_index | 1 |
也就是说这个属性的长度为 0000 0002 ,静态变量的值索引为 0007,也就是说为常量池中第 7 个常量即 Integer 类型的 0。
6.3.6 方法集合
方法表的结构跟字段表完全一致:
类型 | 名称 | 含义 | 数量 |
---|---|---|---|
u2 | access_flags | 访问标志 | 1 |
u2 | name_index | 字段的简单名称 | 1 |
u2 | descriptor_index | 字段的描述符 | 1 |
u2 | attributes_count | 字段属性(例如 Deprecated)的长度 | 1 |
attribute_info | attributes | 属性具体值 | attributes_count |
但是 access_flags 表却是有一部分变化的:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否是 public |
ACC_PRIVATE | 0x0002 | 方法是否是 private |
ACC_PROTECTED | 0x0004 | 方法是否是 proctected |
ACC_STATIC | 0x0008 | 方法是否是 static |
ACC_FINAL | 0x0010 | 方法是否是 final |
ACC_SYNCHRONIZED | 0x0020 | 方法是否是 synchronized |
ACC_BRIDGE | 0x0040 | 方法是否是由编译器产生的桥接方法 |
ACC_VARARGS | 0x0010 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0010 | 方法是否是 native |
ACC_ABSTRACT | 0x0010 | 方法是否是 abstract |
ACC_STRICTFP | 0x0010 | 方法是否是 strictfp |
ACC_SYNTHETIC | 0x0010 | 方法是否是 由编译器自动生成的 |
知道含义再来看图:
字段表后,紧跟着的是 u2 类型方法集合的个数,即 0002 ,也就是说有两个方法。第一个方法是 0001 的,也就是 public 的,名称是 0008 也就是常量池中第 8 个变量,也就是 ,这里说的是无参构造函数。descriptor 是 0009 就是第九个常量 ()V ,代表是 void 类型的无参函数,属性有几个呢,有 0001 个,名称是 常量池中第 000a 个常量 Code ,这个 Code 是什么呢。其实就是我们方法中的方法体部分,结构如下:
那接着一个一个看:
-
属性的长度是 0000 002f 也就是 47 。
-
max_stack(操作数栈深度的最大值)是 0001 。
-
max_locals(局部变量表所需要的存储空间)是 0001 ,这里 0001 单位为slot即插槽。像 byte、char 等长度不超过32位的数据类型,每个占 1 slot 而 double、long 这种 64 位的占用 2 slot。方法参数(包括 this)、显示异常处理器的参数(try-catch 块中定义的 Exception 变量)、方法体中定义的局部变量都需要使用局部变量表来存放。这里需要额外说明的是,**不是把所有变量所占 slot 之和作为 max_locals 的!!**原因是这个局部变量所占的 slot 可以重用,比如 for 循环里的变量在 for 循环结束后,就会被释放。那么这份儿 slot 就可以被重用,Javac 编译器会根据变量的作用域来分配 slot 给各个变量使用,然后计算出 max_locals 的大小。
-
code_length 代码长度,code 是用来存储 Java 源程序编译后生成的字节码指令。code_length 自然就是存储长度。 此处为 0000 0005 即长度为 5。
-
code 是用来存储 Java 源程序编译后生成的字节码指令。每个 u1 类型的数据代表的就是一条指令,具体指令表请看本栏文章《 深入理解Java虚拟机-附件3 虚拟机字节码指令表》,接着我们看看下面 5 个指令都是什么:
- 2a:aload_0,将第一个引用类型本地变量推送至栈顶,意思就是将第 0 个 slot 中为 reference 类型的本地变量推送到操作数栈顶。
- b7:invokespecial 调用超类构造方法,实例初始化方法,私有方法。作用是以栈顶的 reference 类型的数据所指向的对象作为方法接受者,调用此对象的实例构造器方法、private 方法或者他的父类的方法。这个方法有一个 u2 类型的参数说明具体调用的哪个方法,他指向产量高为昂池中的一个 CONSTANT_Methodref_info 类型常量,即此方法的方法符号引用。
- 00 01:上一个指令 invokespecial 的参数,即常量池中第一个常量:
- b1:从当前返回 void。
-
exception_table_length:异常表长度,为 u2 类型数据,即 0000 。
-
exception_info:异常表,记录了显式异常处理表集合。这里没有异常,故为空。
-
attributes_count:属性长度
-
attribute_info:属性。
一个完整 class 字节码到这里结构基本完成了。剩下的代码就是 attribute 和方法字节码了,就不一个个分析了。
6.4 字节码指令简介
虚拟机的指令由一个字节长度的、代表某种特定操作含义的数字(操作码,Opcode),以及跟随其后的零个或者多个参数(操作数,Operands)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数指令都不包含操作数,只有一个操作码。
6.4.1 字节码与数据类型
在 JVM 中,大多数的指令都包含了其操作所对应的数据类型信息。例如 iload 指令用于从局部变量表中加载 int 型的数据到操作数栈中,而 fload 则是加载 float 型的。这两条指令的操作在虚拟机内部可能会是由同一段代码实现的,但是在 Class 文件中必须拥有自己独立的操作码。
大部分与数据类型有关的指令,他们的操作码助记符中都有特殊的字符来表示专门为哪种数据来服务:i 代表 int ,f 代表 float,l 代表 long,s 代表 short,b 代表byte,c 代表 char,d 代表double, a 代表 reference(引用)。但是也有没明确的指明操作类型的,例如 arraylength 指令。或无条件跳转指令 goto 则是与数据类型无关的。
因为 Java 虚拟机的操作码长度只有一个字节,就代表着操作码总数只能有 FF 即 255 个,如果每一种跟数据相关的指令都支持 Java 虚拟机所有运行时数类型的话,那指令的数量恐怕就会超出一个字节所能表示的数量范围了。所以针对特定的操作只提供了有限的类型相关指令去支持他,有些单独的指令可以在必要的时候用来将一些不支持的类型转换为可被支持的类型。
下表列举了Java虚拟机所支持的与数据类型相关的字节码指令,通过使用数据类型列所代表的特殊字符替换opcode列的指令模板中的T,就可以得到一个具体的字节码指令。如果在表中指令模板与数据类型两列共同确定的格为空,则说明虚拟机不支持对这种数据类型执行这项操作。例如,load指令有操作int类型的iload,但是没有操作byte类型的同类指令。
opcode | byte | short | int | long | float | double | char | reference |
---|---|---|---|---|---|---|---|---|
Tstore | istore | lstore | fstore | dstore | astore | |||
Tinc | iinc | |||||||
Taload | baload | saload | iaload | laload | faload | daload | caload | aaload |
Tastore | bastore | sastore | iastore | lastore | fastore | dastore | castore | astore |
Tadd | iadd | ladd | fadd | dadd | ||||
Tsub | isub | lsub | fsub | dsub | ||||
Tmul | imul | lmul | fmul | dmul | ||||
Tdiv | idiv | ldiv | fdiv | ddiv | ||||
Trem | irem | lrem | frem | drem | ||||
Tneg | ineg | lneg | fneg | dneg | ||||
Tshl | ishl | lshl | ||||||
Tshr | ishr | lshr | ||||||
Tushr | iushr | lushr | ||||||
Tand | iand | land | ||||||
Tor | ior | lor | ||||||
Txor | ixor | lxor | ||||||
i2T | i2b | i2s | i2l | i2f | i2d | |||
l2T | l2i | l2f | l2d | |||||
f2T | f2i | f2l | f2d | |||||
d2T | d2i | d2l | d2f | |||||
Tcmp | lcmp | |||||||
Tcmpl | fcmpl | dcmpl | ||||||
Tcmpg | fcmpg | dcmpg | ||||||
if_TempOP | if_iempOP | if_aempOP | ||||||
Treturn | ireturn | lreturn | freturn | dreturn | areturn |
从表中可以看出,大部分的指令都没有支持 byte、char 和 short 类型,甚至没有任何指令支持boolean类型。编译器会在编译器或运行期将byte和short类型的数据带符号扩展(Sign-Extend)为相应的int类型数据,将boolean和char类型数据零位扩展(Zero-Extend)为相应的int类型数据。与之类似,在处理boolean、byte、short和char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。因此,大多数对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的int类型作为运算类型(Computational Type)。
6.4.2 ~ 6.4.10
略,详细请参考本栏文章《 深入理解Java虚拟机-附件3 虚拟机字节码指令表》
总结
Class 文件结构是后面类加载、指令重排序的基础。我这边扣得想尽量详细,所以一步步按照字节码扣下来。虽然花的时间很长但是还是有意义的。这里看到两个博主的图非常形象具体,忍不住盗了过来(嗨呀贼臭不要脸)。分别是
@亦山 大大的博客《Java虚拟机原理图解》 1.1、class文件基本组织结构:
和 @素小暖 大大的《素小暖讲JVM:第六章 类文件结构,第七章 类加载机制,第八章 字节码执行引擎》:
读书越多越发现自己的无知,Keep Fighting!
本文仅是在自我学习 《深入理解Java虚拟机》这本书后进行的自我总结,有错欢迎友善指正。
欢迎友善交流,不喜勿喷~
Hope can help~