
简介
java语言能够实现跨平台,完全依赖于字节码文件
和 JVM虚拟机
,其字节码文件
是一段代码从编写到执行的中间产物,但是对于编写java语言的我们来说,可以不用去关心实现细节的。字节码文件规范其实也是一门艺术,所有的跨平台语言都是基于class文件进行跨平台,所以,这也是扩展的一个知识面。
本篇文章,我们将讨论class文件作为一个二进制字节流,class文件是如何表达源代码的思想的。
练习
我们通过一个简单的例子来学习一下字节码文件
1 | package com.mashibing.jvm.c1_bytecode; |
这是一个简单的类,什么也没有,但是当反编译后,可以看到多了一个构造方法
1 | package com.mashibing.jvm.c1_bytecode; |
用javac命令
编译到的二进制文件为
1 | cafe babe 0000 0034 0010 0a00 0300 0d07 |
结构
接下来,我们分析一下字节码文件

魔数
从深入理解计算机
中我们知道所有的文件(不管是数据还是程序),在计算机里都是以二进制的格式进行保存的。那么如何区分文件的格式呢,我们可以打开png、jpg类型图片,可以发现前四位
1 | -- png格式 8950 4E47 |
同样格式的文件前四位都是一样的。在java语言规范中,标识为class文件的前四位为cafe babe
,这个就叫做魔数
,占四位字节
。
版本号
不同的源码,在不同版本的jdk上编译,得到的字节码也是不一样的,这是通过版本号
来区分的。版本号四个字节
,前面为副版本号
,接着是主版本号
。
因此副版本号
为0x0000
,主版本号
为ox0034
,十进制组合为52.0版本,也就是jdk1.8.0
。
常量池
常量池是字节码设计最复杂的一部分,它的长度不固定,不同类型的常量结构也不一样。如下表:

注:此表格的类型的单位不对,不是bit,应该是byte(字节)。后面的同理。
常量池计数器
记录常量池的长度,占两个字节
。0x0010
表示常量池的长度为16。注意,下标是从1开始的,为0的下标不指向内部引用,是为别的引用预备的。
常量池类型和结构
1 | 0a00 0300 0d07 000e 0700 0f01 0006 3c69 6e69 743e 0100 0328 2956 0100 0443 6f64 6501 000f 4c69 |
这部分二进制描述的全部是常量池的信息,可以对造上面表格一一查找。
比如
0x0a00 0300 0d
查找结构为0a | 0003 | 000d
,即对应10,CONSTANT_Methodref_info类型,接着是方法的两个指针索引,指向下标为3和13的常量池。
这一一查找挺麻烦的,但是我们只要用
1 | javap -verbose T0100_ByteCode01.class |
常量池类型和结构就全都出来了
1 | Constant pool: |
访问标志
常量池后面就是访问标志,用两个字节
来表示,其标识了类或者接口的访问信息,比如:该Class文件是类还是接口,是否被定义成public
,是否是abstract
,如果是类,是否被声明成final
等等。各种访问标志如下所示:

由于所有类都会继承Object类,所以访问标识ox0020
都是满足的,因此访问标志为0x0021
当前类名
两个字节
,类索引的值为ox0002
,指向常量池第二项的索引。
1 | 2 = Class #14 // com/mashibing/jvm/c1_bytecode/T0100_ByteCode01 |
这里是通过类索引我们可以确定类的全限定名。
父类名
跟当前类名一样,两个字节
,类索引的值为ox0003
,指向常量池第三项的索引。
1 | 3 = Class #15 // java/lang/Object |
从字节码的设计来看,这里只有两个字节存父类名的索引,因此,这里只能指向一个类。即从设计上看出,java语言类只能从一个类继承,但多个类可以多重继承。
接口
接口计数器
两个字节
,记录了实现接口的个数。这里为0x0000
,即没有实现接口,因此,接口索引就不占字节。
接口索引
一个接口,两个字节
。通过类索引找到实现的接口全限定名。
字段表
字段计数器
两个字节
,记录了字段的个数。由于没有,故字节码到ox0000
就没了。
字段表结构
字段表作为一个表,同样有他自己的结构

我们知道,一个字段可以被各种关键字去修饰,比如:作用域修饰符(public、private、protected
)、static
修饰符、final
修饰符、volatile
修饰符等等。因此,其可像类的访问标志那样,使用一些标志来标记字段。字段的访问标志有如下这些:

方法表
方法计数器
两个字节
,记录了方法的个数。这里为0x0001
,由于该类没有自定义构造方法,编译后会生成一个构造方法,当初始化的时候会调用Object的构造方法。
方法表结构
方法表的结构实际跟字段表是一样的,方法表结构如下:

跟字段表一样,方法表也有访问标志,而且他们的标志有部分相同,部分则不同,方法表的具体访问标志如下:

对应字节码为:0001 0001 0004 0005 0001 0006 0000 002f 0001 0001 0000 0005 2ab7 0001 b1
1 | 0001 || 0001 || 0004 || 0005 || 0001 || 0006 |
Code属性表结构

1 | 0006 0000 002f |
0x0006
对应常量池索引6的下标,查看是Code类型
0x0000002f
为属性的长度,这里的值是47,即往后47个字节都是Code的内容。
max_stack的值为0x0001
,即操作数栈深度的最大值为1。
max_locals的值为0x0001
,即局部变量表所需的存储空间为1;max_locals的单位是Slot
,Slot是虚拟机为局部变量分配内存所使用的最小单位。
code_length的值为0x000000005
,即字节码指令的长度为5。
code的值为2ab7 0001 b1
,这里的值就代表一系列的字节码指令。一个字节代表一个指令,一个指令可能有参数也可能没参数,如果有参数,则其后面字节码就是他的参数;如果没参数,后面的字节码就是下一条指令。
1 | 2a 指令,查表可得指令为aload_0,其含义为:将第0个Slot中为reference类型的本地变量推送到操作数栈顶。 |
exception_table_length的值为0x0000
,即异常表长度为0,所以其异常表也没有;
attributes_count的值为0x0002
,即code属性表里面还有两个其他的属性表,分别是0x0007
和0x0008
的索引。对应为LineNumberTable属性和LineVariableTable属性。关于这两个属性可以见下面,有特定的结构。
属性表
到这里,我们剩下的字节码还有00 0100 0b00 0000 0200 0c
属性计数器
两个字节
,记录了属性的个数。这里是0x0001
,即一个属性。
属性结构体
属性表的结构比较灵活,各种不同的属性只要满足以下结构即可:

0x000b
对应常量池索引11的下标,查看是SourceFile类型,到这里我们需要查看SourceFile类型属性的结构

属性的长度为0x00000002
,即两个字节,后面是0x000c
指向索引为12的下标,即T0100_ByteCode01.java,表示源文件的名称
其他属性
其实,上面LineNumberTable、LineVariableTable也是类似SourceFile类型,也有特定的结构。Java虚拟机中预定义的属性有20多个,如下图所示

总结
字节码文件的设计真的是一门艺术,根据java语言的规范,将根据人的思想编写的高级语言经过编译器编译成字节码文件,通过二进制(十六进制)表达了源代码的思想。我们在学习的过程中可以使用JClassLib—IDEA插件协助我们看字节码文件,这样就不用面对枯燥的十六进制了。
使用工具查看很是方便,如图
