title: Java字节码(一):深度分析Class类文件
date: 2019-03-27 15:58:04
categories:
- Java虚拟机
tags: - Java字节码
引言
我们知道,java是一个跟平台无关性的编程语言,而平台无关性的基础就是虚拟机与字节码存储格式。Java虚拟机不和包括java语言在内的任何语言绑定,它只认Class文件(kotlin、scala等皆可在jvm上运行)。Class文件中包含了一个Java程序的指令集和符号集以及其他信息,编译器严格按照规范来将Java程序编译为字节码。在这儿,我们分析下Class文件的数据结构。
Class类文件结构
Class文件是以8位字节为基础的二进制流文件,其中没有间隔,在文件中只保留了必要的数据,节省了大量的空间。在Class字节码中有两种数据类型:
- 字节数据直接量:这是基本的数据类型。共细分为
u1、u2、u4、u8四种,分别代表连续的1个字节、4个字节、8个字节组成的整体数据。 - 表(数组):表是由多个基本数据或其他表,按照既定顺序组成的大的数据集合。表是有结构的,它的结构体现在:组成表的成分所在的位置和顺序都是已经严格定义好的。
如下图所示,Class文件中字节码按一下顺序进行排列。
我们通过最简单的一个程序,来示例讲解Class文件结构。
public class MyTest1 {
private int a = 1;
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}
使用16进制工具(winHex)打开MyTest1.class文件,16进制字节码中,一个数字或字母占4位,两个数字或字母代表一个字节。
使用javap -verbose命令分析一个字节码文件时,将会分析该字节码文件的魔数、版本号、常量池、类信息、类的构造方法、类中的构造方法、类中的方法信息、类变量与成员变量等信息。
魔数(4个字节)
所有的.class字节码文件的前4个字节都是魔数,魔数值是一固定值:0xCAFEBABE。 是用以Java虚拟机确定class文件的标志。很多文件格式都采用了魔数来进行文件的身份标识,比如jpg。选择在文件内容头部使用魔数而不用扩展名来进行文件的身份标识,主要是考虑了安全问题,因为扩展名易被修改。而魔数则可以由文件格式的制定者随意的指定,只要选择的魔数没有被广泛采用且不与其他魔数重复引起混淆就行。
版本号(2+2个字节)
.魔数之后的4个字节为版本信息,前两个字节表示minor version(次版本号),后两个字节表示major version(主版本号)。这里00 00 00 34,换算成十进制,表示次版本号为0,主版本号52。表示java版本1.8.0,1.8表示主版本号,0表示次版本号。可以使用java -version查看。JDK是向下兼容的,高版本的JDK能运行低于此版本的Class文件,低版本的JDK无法运行高于它本身的Class文件,即使文件格式未出错,虚拟机也拒绝执行高于其版本的Class文件。若进行运行,会报错java.lang.UnsupportedClassVersionError。
常量池(2+n个字节)
常量池(constant pool):紧接着主版本号之后的就是常量池入口。一个Java类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看作是Class文件的资源仓库,比如说Java类中定义的方法与变量信息,都是存储在常量池中。
常量池中主要存储两类常量:字面量和符号引用。
- 字面量如文本字符串,
Java中声明为final的常量值等。 - 符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符。
常量池的总体结构
Java类所对应的常量池主要由常量池与常量池数组(常量表)这两部分共同构成。
-
常量池数量紧跟在主版本号后面,占据
2个字节 -
常量池数组则紧跟在常量池数量之后。
常量池数组与一般的数组不同的是,常量池数组中不同的元素的类型、结构都是不同的,长度当然也就不同,这些元素是被称之为表的数据结构;但是,每一种元素的第一个数据都是一个
u1类型,该字节是个标志位,占据一个字节。JVM在解析常量池时,会根据这个u1类型来获取元素的具体类型。值得注意的是,常量池数组中元素的个数 = 常量池数量 - 1 (其中0位暂时不使用),根本原因在于,索引0也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应
null值;所以,常量池的索引从1而非0开始。
常量池中的描述信息
- 在
JVM规范中,每个变量/字段都有描述信息,描述信息主要的作用是描述字段的数据类型、方法的参数列表(包括数量、类型、顺序)与返回值。根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符来表示,对象类型则使用字符L加对象的全限定名称来表示,为了压缩字节码文件的体积,对于基本数据类型,JVM都只使用一个大写字母来表示,如:B - byte,C - char,D - double,F - float,I - long,s - short,Z - boolean,V - void,L - 对象类型,如Ljava/lang/String; - 对于数组类型来说,每一个维度都使用一个
[来表示,如int[]被记录为[I,String,String[][][]被记录为[[Ljava/lang/String; - 用描述符描述方法时,按照先参数列表,后返回值的顺序来描述。参数列表按照参数的严格顺序放在一组
()之内,如方法:String test(int a,String b)的描述为:(I,Ljava/lang/String)Ljava/lang/String;
示例代码的常量池
常量池数量:
-
第9、10个字节代表着常量池数量00 18,即24-1=23。
常量池数组:
-
第11个字节是
0A(tag值)= 10 ,为CONSTANT_Meothdref_info,这个类型有两个index值,占4个字节00 04 00 14,即为常量池中第4个元素,和第20个元素。java/lang/Object.""????)V
表示无参数列表的返回值为void的方法,即虚拟机创建的构造方法。
-
第16个字节为09 = 9 ,为
CONSTANT_Fieldref_info,后四个字节为两个索引00 03 00 15
即第3个元素,第21个元素。bytecode/MyTest1.a:I
表示MyTest1类下的a属性的值为int类型 -
第21个字节为07,为
CONSTANT_Class_info,后2个字节为指向全限定名常量项的索引,指向22bytecode/MyTest1
表示此类的全限定名
-
第24字节为07,00 17指向#23。
java/lang/Object
表示父类的全限定名 -
第27字节为01,为
CONSTANT_utf8_info,后2位字节为UTF-8编码的字符串长度length00 01,表示后面有一个字节来表示这个字符,即为61 =a后面相同的类型,就简写了。
-
第31字节为01,00 01,字符串为49 =
I -
第35字节为01,00 06,字符串
3C 69 6E 69 74 3E,表示为<init> -
第43字节为01 ,00 03,28 29 56,表示为
()V -
第50字节为01,00 04,43 6F 64 65,表示为
Code -
第57字节为01,00 0F,表示为
LineNumberTable -
第75字节01 ,00 12,表示为
LocalVariableTable -
第95字节01, 00 04 ,
this -
第83字节01,00 12,
Lbytecode/MyTest1; -
第109字节01,00 04,
getA -
第131字节01,00 03,
()I -
第136字节01,00 04,
setA -
第143字节01, 00 04,
(I)V -
第151字节01,00 01,
SourceFile -
第163字节01,00 0C,
MyTest1.java -
第179字节0C,为
CONSTANT_NameAndType_info,后四个字节指向方法名称#7;方法描述#8“”????)V
-
第184字节0C,为
CONSTANT_NameAndType_info,后四个字节指向字段名称#5;字段描述#6a:I
-
第189字节01,00 10,
bytecode/MyTest1 -
第207字节01,00 10,
java/lang/Object
类的访问权限(2个字节)
接下来是类的访问权限access_flag,第227、228字节00 21:是0x0020和0x0001的并集,表示ACC_PUBLIC与ACC_SUPER。
类名(2个字节)
第229、230字节00 03为类名,是一个引用值,表示常量池中第三个常量。当前类的全限定名。
父类名(2个字节)
第231、232字节00 04为父类名,是一个引用值,表示常量池中第四个常量。当前类的父类全限定名。
接口(2+n个字节)
第233、234字节00 00为接口数,接口数为0,后面的接口名就不会出现了。
域(2+n个字节)
域的个数
第235、236字节00 01为成员变量数,为1个。
字段表集合
字段表用于描述类和接口中声明的变量。这里的字段包含了类级别的变量以及实例变量,但是不包括方法内部声明的局部变量,下图为字段表的结构。
本例中只有一个字段。
- 第237、238字节00 02为这个成员变量的权限访问符,表示
privat。 - 第239、240字节00 05为这个成员变量的名称描述符,指向常量池中的#5 =
a。 - 第241、242字节00 06为这个成员变量的类型描述符,指向常量池中的#6 =
I。 - 第243、244字节00 00为附加属性个数,为0,则附加属性表就不会出现。附加属性是有些编译器在编译阶段添加的值。
方法(2+n个字节)
方法个数
第245、246字节00 03为方法数,共3个方法。
方法表集合
方法表描述了一个方法的信息。
属性表集合描述了这个方法的相关属性,执行码、异常表等,JVM预定义了部分属性,但是编译器自己也可以实现自己的属性写入class文件里,供运行时使用。
不用的属性通过attribute_name_index来区分。
NO.1
-
第247、248字节00 01为权限访问符,表示
public。 -
第249、250字节00 07为这个方法的名称描述符,指向常量池中的#7 =
<init> -
第251、252字节00 08为这个方法的类型描述符,指向常量池中的#8 =
()V -
第253、254字节00 01为附加属性个数。这些属性用以描述方法,由
Java虚拟机根据方法的执行代码编译时计算出来的。 -
属性信息
attribute_info:Code attribute的作用是保存改方法的结构,如下图。-
attribute_length表示attribute所包含的字节数,不包含attribute_name_index和attribute_length字段。 -
max_stack表示这个方法运行的任何时刻所能达到的操作数栈的最大深度。 -
max_locals表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量。 - code_length表示该方法所包含的字节码的字节数以及具体的指令码。
- 具体字节码即是该方法被调用时,虚拟机所执行的字节码。
-
exception_table,这里存放的是处理异常的信息。 - 每个
exception_table表现由start_pc,end_pc,hander_pc,catch_type组成。
-
第255、256字节00 09为属性值的索引,#9 为
Code,表示执行代码。 -
第257、258、259、260字节00 00 00 38为属性长度,为56字节。
-
第261、262字节00 02为操作数栈的最大深度,栈深度为2。
-
第263、264字节00 01为局部变量个数。
-
第265、266、267、268字节00 00 00 0A为这个方法包含的字节数与字节码,一共占10个字节,代表这个方法真正执行的内容(也就是助记符,在字节码中只是一些16进制的符号,转换为助记符,帮助我们记忆)。
- 第269字节2A,对应助记符
aload_0,从局部变量表中位于0slot中的变量推到操作数栈中去。 - 对应助记符
invokespecial,调用父类的方法。- 第271、272字节00 01代表这个助记符的参数 ,指向常量池#1,
java/lang/Object."<init>":()V,即代表调用Object类的这个方法,即父类的构造方法。
- 第271、272字节00 01代表这个助记符的参数 ,指向常量池#1,
- 第273字节2A,对应助记符
aload_0。 - 第274字节04 ,对应助记符
iconst_1,将int类型的数1推送到操作数栈顶。 - 第275字节B5 ,对应助记符
putfield。- 第276、277字节00 02代表这个助记符的参数,指向常量池#2,
bytecode/MyTest1.a:I,也就是说将刚刚推到操作数栈顶的1赋予MyTest1中的a。
- 第276、277字节00 02代表这个助记符的参数,指向常量池#2,
- 第278字节B1,对应助记符
return,返回void。
- 第269字节2A,对应助记符
-
第279、280字节00 00,异常表长度为0,异常表就不会出现了。
-
第281、282字节00 02,表示附加属性个数。
首先是
LineNumTable,行号信息,通过这个信息,可以确定,代码的所在的行数。- 第283、284字节00 0A,代表附加属性索引,指向常量池#10,
LineNumberTable - 第285、286、287、288字节00 00 00 0A,代表这个附加属性的长度。
- 第289、290字节00 02,代表有两个映射。
- 第291、292字节00 00,
- 第293、294字节00 0F,表示
code数组中偏移量为0的,映射到第10行,在第10行,编译器生成了一个构造方法。 - 第295、296字节00 04,
- 第297、298字节00 11,表示code数组中偏移量为4的,映射到第17行,
然后是LocalVariableTable,局部变量表。 - 第299、300字节00 0B,代表附加属性索引,指向常量池#11,
LocalVariableTable - 第301、302、303、304字节00 00 00 0C代表这个附加属性的长度。
- 第305、306字节00 01 ,代表只有一个变量。
- 第307、308字节00 00,代表局部变量从0开始。
- 第309、310字节00 0A,代表局部变量长度。
11和12决定了局部变量的作用范围是从哪一行到哪一行。
- 第311字节00,代表局部变量索引。
- 第312字节0C,代表常量池中的#10,
this,代表当前对象。 - 第313、314字节00 0D,表示词局部变量的描述,代表常量池中的#13,
Lbytecode/MyTest1; - 第315、316字节00 00,做校验检查的。
在Java中,每个方法都可以访问this,对于非静态方法来说,至少有一个局部变量传入方法,即this。
- 第283、284字节00 0A,代表附加属性索引,指向常量池#10,
-
NO.2
-
第317、318字节00 01为权限访问符,表示
public。 -
第319、320字节00 0E为这个方法的名称描述符,指向常量池中的#14 =
getA。 -
第321、322字节00 0F为这个方法的类型描述符,指向常量池中的#15 =
()I。 -
第323、324字节00 01为附加属性个数。这些属性用以描述方法,由
Java虚拟机根据方法的执行代码编译时计算出来的。 -
属性信息
attribute_info:-
第325、326字节00 09为属性值的索引,#9 为Code,表示执行代码
-
第327、328、329、330字节00 00 00 2F为属性长度
-
第331、332字节00 01为操作数栈的最大深度
-
第333、334字节00 01为局部变量表最大长度
-
第335、336、337、338字节00 00 00 05为这个方法包含的字节数与字节码,一共占5个字节,代表这个方法真正执行的内容(助记符,在字节码中只是一些16进制的符号,转换为助记符,帮助我们记忆
-
第339字节2A,对应助记符aload_0,
-
第340字节B4,对应助记符getfiled,调用父类的方法
-
第341、342字节00 02代表这个助记符的参数 ,指向常量池#2, // bytecode/MyTest1.a:I
表示从对象中获取字段 -
第343字节AC,对应助记符ireturn,表示返回一个int
-
第344、345字节00 00,异常表长度为0,异常表就不会出现了
-
第346、347字节00 02,表示附加属性个数:
首先是LineNumTable,行号信息
- 第348、349字节00 0A,代表附加属性索引,指向常量池#10,LineNumberTable
- 第350、351、352、353字节00 00 00 06,代表这个附加属性的长度
- 第353、354字节00 01,代表有1个映射
- 第355、356字节00 00,
- 第357、358字节14 00,
- 表示code数组中偏移量为0的,映射到第20行,在第20行,对应return a;
然后是LocalVariableTable,局部变量表
- 第359、360字节00 0B,代表附加属性索引,指向常量池#11,LocalVariableTable
- 第361、362、363、364字节00 00 00 0C代表这个附加属性的长度
- 第365、366字节00 01 ,代表只有一个变量
- 第367、368字节00 00,代表局部变量从0开始
- 第369、370字节00 05,代表局部变量长度
- 第371字节00,代表局部变量索引
- 第372字节0C,代表常量池中的#10,this,代表当前对象
- 第373、374字节00 0D,表示词局部变量的描述,代表常量池中的#13, Lbytecode/MyTest1;
- 第376、377字节00 00,做校验检查的
-
-
NO.3
第三个方法就不再叙述了,与第二个方法差不多。
从上文看出,当一个
Java程序中没有构造方法时,编译器会生成一个<init>方法,即构造方法。且非静态成员变量赋值是在构造方法中完成的。若有多个构造方法,每个构造方法都会完成非静态成员变量赋值。
字节属性
- 第456、457字节00 01,只有一个属性。
- 第458、459字节00 12,指向#18 ,
SourceFile。 - 第460、461、462、463字节00 00 00 02,表示属性长度,占两个字节。
- 第464、465字节00 13,表示#13,
MyTest1.java。