Java字节码:深度分析Class类文件
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
编码的字符串长度length
00 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
。