Java字节码的文件结构解析

1、字节码文件产生缘由

java语言在其诞生之初就有一句口号:“一次编写,到处运行”,这句话也表达了大多数开发人员对于冲破平台界限的一种渴望,因为以前的编程语言都不支持跨平台运行。就拿C语言作为一个例子,我们知道在不同的操作系统中的cpu指令集是不一样的例如windows只支持x86系列,linux即有支持ARM的,也有支持x86的,这就导致一个结果——C语言在不同操作系统编译后产生的二进制序列运行效果是不一样的,它就必须得在不同的操作系统上安装不同的编译器才能在不同的操作系统上运行。而Java则用了一个虚拟机的概念,Java的源代码经过编译后产生.class为后缀的文件,这就是我们所说的字节码文件。
用一小段代码进行演示(本文其它部分也用该段代码进行讲解):
源代码TestClass.java:

1
2
3
4
5
6
7
package org.fenixsoft.clazz;
public class TestClass{
private int m;
public int inc(){
return m+1;
}
}

用javac命令进行编译生成TestClass.class文件,然后用winhex这个16进制编辑器进行查看
二进制class文件
这种文件可以在所有的Java虚拟机上运行,不管你是什么操作系统。从而实现跨平台运行,虽然不同平台的Java虚拟机的实现各不相同。这种虚拟机是真实操作系统的一个抽象,逻辑上可以理解为一个独立的平台,利用这个平台实现跨操作系统运行。
除了平台无关之外,Java虚拟机还实现了语言无关,因为Java虚拟机只和class文件具有“绑定关系”,但是除Java语言外的其它语言如果能编译成class文件的话也能在虚拟机上运行。已经有JRuby和Groovy等语言能在Java虚拟机上运行。
总的来说,虚拟机的引入是实现平台无关性和语言无关性的一个关键,对与字节码文件的研究对于理解Java和Java虚拟机有很大的帮助,下面我们就来深入理解一下这个字节码文件的结构。

2、class类文件结构

Java的class文件时一组以8位的字节为基础单位的二进制,根据Java虚拟机规定,Class文件格式采用一种类似c语言的结构体来存储数据,这种伪结构中只有两种数据类型:无符号数和表。

  • 无符号数:数据基本的数据类型,以u1、u2、u4、u8分别表示一个字节,2个字节,4个字节和8个字节的无符号数,它可以用老表示数字,索引、数据值或者按照utf-8编码的字符串。
  • 表:由多个无符号数或者其它表作为数据项构成复合数据类型

    2.1、魔数和class文件版本

    我从这里开始逐字节分析class文件。每个class文件的头4 个字节称为“魔数”,它的作用的标注文件类型,确定这个文件是否是能被虚拟机接收的class文件,虚拟机之所以不用文件后缀名作为判断依据的原因是文件后缀名可以任意改动。例如本文中TestClass.class文件中的魔数是:0xCAFEBABE(咖啡宝贝……),这个魔数在Java还被称为Oak的时候就已经被定下来了。Java的商标大概就是因此定下来的。
    紧接着魔数的下面4个字节存储的是Class文件的版本号:第5,6字节是次版本号,第7,8字节是主版本号。Java的版本号是从45开始的,JDK1.1后每个JDK大版本发布主版本号都往上+1,高版本的JDK兼容以前版本的Class文件,但不能运行以后版本的class文件,即使格式没变,虚拟机也拒绝执行。本例中的class文件的次版本号是0x0000,主版本号是0x0034,它是十进制是52,说明我用的JDK版本是JDK1.8。
    魔数和class版本

2.2、常量池

常量池的入口在主次版本之后,常量池中存放着class的资源,它与class文件中其它项目关联最大,它也是class文件中第一个出现的表类型的数据项目。
常量池入口的的一个大小为2u类型(2个字节)的数据被称为常量池容量计数器(constant_pool_count),代表常量池中的常量数量。本例中的常量池容器计数器值是0x13,其十进制是19,但是这个容量计数器是从1开始的,设计者将第0项做了特殊考虑,是为了满足后面某些指向常量池的数据可能会需要表达“不引用任何常量池”的项目,这种情况下可以把索引值设置为0来表示,所以本例中的常量数量是19-1=18项。这18个常量紧跟在常量计数器后面。
下面来介绍在常量池中常量的类型已经其结构。
常量池中的每一项常量都是一张表,在JDK1.7之前共有11个结构各不相同的表结构数据,在JDK1.7中为了更好的支持动态语言的调用,又加了三个(CONSTANT_MethodHandle_info,CONSTANT_MethodType_info,CONSTANT_InvokeDynamic_info),这14种表都有一个共同点,就是表的第一位都是一个u1类型的标志位tag代表当前这个变量数据哪种常量类型。而其它位置的数据并不统一,各具其结构。其具体的结构如下。
常量池
可以根据这个表逐个分析这些常量,我逐个把这18个常量进行圈出来(可能有点看不清)
字节码中的常量
最左边的箭头是指承接上一层的框框。
为了更方便的观察常量池的结构,可以用Javap命令进行可视化输出,在命令行输入

javap -verbose TestClass

我截取常量池部分的输出
常量池输出
其中第一列是常量类型,第二列表示引用的常量或者utf8类型常量值,例如第一个常量#1的类型名称是Methodref,引用了第4个和第15个常量,第15个常量是NameAndType类型,值又引用了第七个和第八个常量,第七个和第八个常量又是< init >和()V,第四个常量引用第18个常量,第18个常量值是Java/lang/Object(全限类名)。

2.3、访问标志

常量池结束后紧接着的两个字节表示访问标志(access_flag),这个标志用于识别类或者接口的访问信息。2个字节本来是16个标志位,但是只有8个标志位有用,没有使用到的标志一律为0。
访问标志
本例中TestClass是一个普通类,不是接口、美剧或者注解,被public关键词修饰但没有被声明为final和abstract,所有它的ACC_PUBLIC和ACC_SUPER标志位是1,所以本例中的访问标志是0x0021

2.4、类索引、父类索引和接口索引集合

类索引和父类索引都是一个u2类型的数据而接口索引是一组u2类型数据的集合,class通过这三个数据来确定类的继承关系。类索引用于确定类的全限类名,父索引用于确定这个父类的全限类名,接口索引用于描述这个类实现了哪些接口,因为一个类可以实现多个接口。
类索引、父类索引和接口索引按顺序排列在访问标志之后,类索引和父索引分别用一个u2类型的数据,分别指向一个类型是CONSTANT_Class_info类型的类描述符常量,通过其索引值找到CONSTANT_utf8_info中的全限类名字符串。
接口索引人口第一项是一个u2类型的数据为接口计数器(interface_count),表示索引表个数,若没有实现任何接口,其值是0。
本例中的类索引是0x0003,父索引是0x0004,分别表示org.fenixsoft/clazz/TestClass和,java/lang/Object。接口计数器是0x0000,无接口实现。
索引

2.5、字段表集合

字段表集合分为容量计数器(fields_count)和字段表,字段计数器表示字段表个数。
字段表用于描述接口或者类中申明的变量,字段修饰符放在access_flag中,它与访问标志的access_flag相似,具体含义见下表
字段表
跟随access_flag后面的是两项索引值,name_index和descriptor_index。它们都是对常量池的引用。分别表示字段的简单名称和字段和方法的描述符。字段描述符的作用是描述字段的数据类型、方法的参数列表和返回值,基本数据类型和void类型都用一个大写字符表示,描述符标识字含义见下表

标识字符 含义
B 基本类型byte
C 基本类型char
D 基本类型double
F 基本类型float
I 基本类型int
J 基本类型long
S 基本类型short
Z 基本类型boolean
V 特殊类型void
L 对象类型如java/lang/Object

对于数组类型,每一维度都使用一个前置’[‘描述,如一个整型数组int[]被描述为“[I”,用描述符描述方法时,参数列表严格顺序放在一组小括号里面,如void inc(int a,int b)的描述符是“(II)V”
本例中容器计数器的值是0x0001,说明有一个字段表,该字段表access_flag是0x0002,说明类型private,name_index和descriptor_index分别是0x00050x0006分别表示m和I说明其类型是int。descriptor_index后面跟一个属性表集合用于存放一些额外的信息。本例属性表计算器是0x0000,说明没有额外信息。
字节码中的字段

2.6、方法表集合

class对方法的描述和对字段的描述几乎采用一样的方式。仅在访问标志和属性表集合有所区别。
方法访问标志是:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 方法是否为public
ACC_PRIVATE 0x0002 方法是否为private
ACC_PROTECTED 0x0004 方法是否为protected
ACC_STATIC 0x0008 方法是否为static
ACC_FINAL 0x0010 方法是否为final
ACC_SYHCHRONRIZED 0x0020 方法是否为synchronized
ACC_BRIDGE 0x0040 方法是否是有编译器产生的方法
ACC_VARARGS 0x0080 方法是否接受参数
ACC_NATIVE 0x0100 方法是否为native
ACC_ABSTRACT 0x0400 方法是否为abstract
ACC_STRICTFP 0x0800 方法是否为strictfp
ACC_SYNTHETIC 0x1000 方法是否是有编译器自动产生的

还是以本例子讲解
容量计数器是0x0002,说明类中有两个方法(分别是类构造器和源码中的inc),其中第一个方法访问标志是0x0001说明是public,名称索引和描述符索引分别是0x00070x0008,分别表示< init>和()V说明是void返回值,无参数。
字节码中的方法表

接下来是属性集合,其容器计数器是0x0001,说明有个属性表,其中第一个属性表的第一个u2数据表示attribute_name_inde是属性表名称,值是0x0009,对于常量池“code”值,说明是个code属性。code属性表结构如表
code属性表集合
本例中属性长度是0x0000001D,maxstack是0x0001,maxlocal是0x0001,codelength是0x00000005,code是0x2AB70001B1,下面是exception属性集合,其计数器是0x0000,说明没有异常。下面有一个属性表,计数器是0x0001,值是0x000A,是LineNumberTable属性,用于描述Java源码行号和字节码行号之间的关系。其attribute_name_index是0x000A是属性名,指LineNumberTable,0x00000006是属性长度,0x0001是line_number_table_length,line_number_info包括了start_pc和line_number两个u2数据项。
字节码中的属性表

3、总结

本文首先对class文件进行简要介绍,随后对的一个真实的.class文件每个字节进行分析深入了解了它的结构,分析它与源代码之间的关系,本文是了解Java虚拟机的基础知识,以科普为主,如有错误欢迎指正。
最后欢迎大家点赞关注

您的鼓励是我最大的动力!