字节码解析
结构
本位将详细介绍字节码的2进制结构和JVM解析2进制流的规范。规范对字节码有非常严格的结构要求,其结构可以用一个JSON来描述:
{
magicNumber: 0xcafebabe,//魔数
minorVersion: 0x00, //副版本号
majorVersion: 0x02, //主版本号
constantPool:{ //常量池集合
length:1,//常量个数
info:[{id:"#1“,type:"UTF8",params:"I"}]//常量具体信息
},
accessFlag:2,//类访问标志
className:constantPool.info[1].id,//类名称,引用常量池数据
superClassName:constantPool.info[2].id,//父类名称,引用常量池数据
interfaces:{length:1,[id:constantPool.info[3].id],//接口集合
fields:{ //字段集合
length:1,//字段个数
info:[{
accessFlag:'PUBLIC', //访问标志
name:constantPool.info[4].id //名称,引用常量池数据
description:constantPool.info[5].id //描述,引用常量池数据
attributes:{length:0,info:[]} //属性集合
}]
},
methods:{ //方法集合
length:2, //方法个数
info:[{
accessFlag:'PUBLIC', //访问标志
name:constantPool.info[4].id //名称,引用常量池数据
description:constantPool.info[5].id //描述,引用常量池数据
attributes:{ //属性集合
length:1, //属性集合长度
info:[{
name:constantPool.info[6].id,//属性名称索引,引用常量池数据
byteLength:6,
info:'', //属性内容,每一种属性结构都不同。
}]}
}]
},
attributes:{length:0,info:[]} //类的属性
}
本文会将下面这一段Java源码编译成字节码,然后一一说明每一个字节是如何解析的:
public class SimpleClass{
private int i;
public int get() {
return i;
}
}
将源码编译成后,会转换成下面2进制流,通常用16进制来展示(1byte=8bit所以1个字节可以用2个16进制数类表示,即0xFF 相当与2进制的1111)。
0 1 2 3 4 5 6 7 8 9 a b c d e f
0: cafe babe 0000 0034 0013 0a00 0400 0f09
1: 0003 0010 0700 1107 0012 0100 0169 0100
2: 0149 0100 063c 696e 6974 3e01 0003 2829
3: 5601 0004 436f 6465 0100 0f4c 696e 654e
4: 756d 6265 7254 6162 6c65 0100 0367 6574
5: 0100 0328 2949 0100 0a53 6f75 7263 6546
6: 696c 6501 0010 5369 6d70 6c65 436c 6173
7: 732e 6a61 7661 0c00 0700 080c 0005 0006
8: 0100 2265 7861 6d70 6c65 2f63 6c61 7373
9: 4c69 6665 6369 636c 652f 5369 6d70 6c65
a: 436c 6173 7301 0010 6a61 7661 2f6c 616e
b: 672f 4f62 6a65 6374 0021 0003 0004 0000
c: 0001 0002 0005 0006 0000 0002 0001 0007
d: 0008 0001 0009 0000 001d 0001 0001 0000
e: 0005 2ab7 0001 b100 0000 0100 0a00 0000
f: 0600 0100 0000 0300 0100 0b00 0c00 0100
10: 0900 0000 1d00 0100 0100 0000 052a b400
11: 02ac 0000 0001 000a 0000 0006 0001 0000
12: 0006 0001 000d 0000 0002 000e 0a
字节码是用2进制的方式紧凑记录,不会留任何缝隙。所有的信息都是靠位置识别。JVM规范已经详细定义每一个位置的数据内容。
文中斜体 ~00~03 表示16进制流的从第一个字节开始的偏移位置。~1d 表示1行d列这1个字段,~00~03 表示0行0列到0行3列这4个字节。每2个16进制数表示一个字节。因此 ~00~03 表示0xcafebabe,一共4个字节。
magicNumber魔数
~00~03 是字节码的魔数。
0 1 2 3 4 5 6 7 8 9 a b c d e f
0: cafe babe
它用于在文件中标记文件类型达到比文件后缀更高的安全性。魔数一共4字节,用16进制的数据显示就是0xcafebabe(11001010111111101011101010111110)。
version版本号
~04~07 是当前字节码的版本号。
0 1 2 3 4 5 6 7 8 9 a b c d e f
0: #### #### 0000 0034
通常情况下低版本的JVM无法执行高版本的字节码。所以每一个编译出来的 .class 文件都会携带版本号。版本号分为2个部分。前2个字节表示副版本号,后2个字节是主版本号。
~04~05:0x0000=>副版本号为0。
~06~07:0x0034=>主版本号为52。
Constant Pool 常量池集合
{ //常量池集合
length:1,//常量个数,2byte
info:[{
id:"#1“, //索引, 1byte
type:"UTF8", // 类型, 1byte
params:"I" //参数,根据类型而定
}]//常量具体信息
}
如上图,常量池是一个集合,他分为集合数量和集合内容部分
常量池个数
0 1 2 3 4 5 6 7 8 9 a b c d e f
0: #### #### #### #### 0013
~08~09 表示常量池的常量个数。常量池的索引可以理解为从0开始的,但是保留#0用来表示什么都不索引。这里的0x0013换算成10进制就是19,表示一共有19个常量——#0~#18。
常量池列表
紧随着常量池索引的是常量池的内容,是一个列表结构。常量池中可以放置14种类型的内容。而每个类型又有自己专门的结构。通常情况下列表中每个元素的第一个字节表示常量的类型(见附录——常量类型),后续几个字节表示索引位置、参数个数等。下面开始解析每一个常量
#1,~0a~0e 是第一个常量,
0 1 2 3 4 5 6 7 8 9 a b c d e f
0: #### #### #### #### #### 0a00 0400 0f##
0x0a=10,查找对应的类型是一个Methodref类型的常量。Methodref的常量按照规范后续紧跟2个分别2字节的常量池索引,所以0x0004=4和0x000f=15,表示2个参数索引到常量池的#4和#15。
#2,~0f~13 是第二个常量,
0 1 2 3 4 5 6 7 8 9 a b c d e f
0: #### #### #### #### #### #### #### ##09
1: 0003 0010
0x09=9,根据常量池类型表索引,这是一个Fieldref类型的常量。他的结构和Methodref一样也是紧跟2个2字节的参数,0x0003和0x0010表示索引常量池的#3和#16。
#3,下一个常量是 ~14~16 ,
0 1 2 3 4 5 6 7 8 9 a b c d e f
1: #### #### 0700 11
0x07表示该位置常量是一个Class 类型,它的参数是一个2字节的常量池索引。0x0011表示索引到常量池#17的位置。
#4,~17~19 是另外一个 Class 类型的常量,~18~19 的参数表示索引到#18位置。
0 1 2 3 4 5 6 7 8 9 a b c d e f
1: #### #### #### ##07 0012
#5,接下来,~1a~1d 是一个 UTF8 类型,
0 1 2 3 4 5 6 7 8 9 a b c d e f
1: #### #### #### #### #### 0100 0169
~1a 的0x01表示这个常量是一个 UTF8 类型。他后续2个字节表示字符串长度,然后就是字符串内容。
~1b~1c:UTF8 的字符串长度,0x0001表示自由一个字符。
~1d:表示字符编码,0x69=105,对应的ASCII就是字符"i"。
字节码采用UTF-8缩略编码的方式编码——''到''的字符串(相当于ASCII 0~127)只用一个字节表示,而'ࠀ'到''的编码使用3个字节表示。
#6,继续往下 ~1e~21 又是一个 UTF8 类型。
0 1 2 3 4 5 6 7 8 9 a b c d e f
1: #### #### #### #### #### #### #### 0100
2: 0149
~1e:0x01表示 UTF8 类型。
~1f~21:0x0001,表示UTF8字符串长度1。
~22:0x49=73,换算成ACSII为"I"。
#7,~22开始还是一个UTF8类型。
0 1 2 3 4 5 6 7 8 9 a b c d e f
2: #### 0100 063c 696e 6974 3e
~22:0x01表示 UTF8 类型。
~23~24:0x0006表示一共包含8个字符。
~25~2a:依次为0x3c='<'、0x69='i'、0x6e='n'、0x69='i'、0x74='t'、0x3e='>',所以这个UTF8所表示的符号为"<init>",代表了一个类的构造方法。
#8,~2b~30是一个长度为3的UTF8类型,值为"()V"。
0 1 2 3 4 5 6 7 8 9 a b c d e f
2: #### #### #### #### #### ##01 0003 2829
3: 56
#9,~31~37: UTF8 ,值为"Code"
0 1 2 3 4 5 6 7 8 9 a b c d e f
3: ##01 0004 436f 6465
#10,~38~49: UTF8 ,值为"LineNumberTable"
0 1 2 3 4 5 6 7 8 9 a b c d e f
3: #### #### #### #### 0100 0f4c 696e 654e
4: 756d 6265 7254 6162 6c65
#11,~4a~4f: UTF8 ,"get",表示我们代码中的get方法的字面名称。
0 1 2 3 4 5 6 7 8 9 a b c d e f
4: #### #### #### #### #### 0100 0367 6574
#12,~50~55: UTF8 ,"()I",表示一个返回整数的方法符号。
0 1 2 3 4 5 6 7 8 9 a b c d e f
5: 0100 0328 2949
#13,~56~62: UTF8 ,长度0x0a,值"SourceFile"。
0 1 2 3 4 5 6 7 8 9 a b c d e f
5: #### #### #### 0100 0a53 6f75 7263 6546
6: 696c 65
#14,~63~75: UTF8 ,"SimpleClass.java",表示当前类的名称
0 1 2 3 4 5 6 7 8 9 a b c d e f
6: #### ##01 0010 5369 6d70 6c65 436c 6173
7: 732e 6a61 7661
#15,~76~7a是一个NameAndType类型(0x0c=12),
0 1 2 3 4 5 6 7 8 9 a b c d e f
7: #### #### #### 0c00 0700 08
NameAndType类型接收2个2字节的参数,代表名称的索引和类型的索引。这里的参数值为0x0007和0x0008,指向常量池的#7和#8位置,刚才已经还原出来#7="<init>",#8="()V"。所以这是一个没有参数返回为void的构造方法。
#16,~7b~7f还是一个NameAndType,2个索引分别指向#5="i",#6="I",这里符号指定的是类中的成员变量i,他一个整数类型。
0 1 2 3 4 5 6 7 8 9 a b c d e f
7: #### #### #### 0c00 0700 08
#17,~80~a4:长度为32的字符串(0x0022=32),值为"example/classLifecicle/SimpleClass"
0 1 2 3 4 5 6 7 8 9 a b c d e f
8: 0100 2265 7861 6d70 6c65 2f63 6c61 7373
9: 4c69 6665 6369 636c 652f 5369 6d70 6c65
a: 436c 6173 73
#18,~a5~b7:长度为16的字符串,值为"java/lang/Object"
0 1 2 3 4 5 6 7 8 9 a b c d e f
a: #### #### ##01 0010 6a61 7661 2f6c 616e
b: 672f 4f62 6a65 6374
到此已经解析完全部18个常量,JVM开始解析之后的访问标志(access_flags)。
accessFlag 访问标志
访问标志就是在Java源码中为类的使用范围和功能提供的限定符号。在一个独立的字节码文件中,仅用2个字节记录,目前定义了8个标志:
标志名称 | 值(16进制) | 位(bit) | 描述 |
PUBLIC | 0x0001 | 0000000000000001 | 对应public类型的类 |
FINAL | 0x0010 | 0000000000010000 | 对应类的final声明 |
SUPER | 0x0020 | 0000000000100000 | 标识JVM的invokespecial新语义 |
INTERFACE | 0x0200 | 0000001000000000 | 接口标志 |
ABSTRACT | 0x0400 | 0000010000000000 | 抽象类标志 |
SYNTHETIC | 0x1000 | 0001000000000000 | 标识这个类并非用户代码产生 |
ANNOTATION | 0x2000 | 0010000000000000 | 标识这是一个注解 |
ENUM | 0x4000 | 0100000000000000 | 标识这是一个枚举 |
访问标志不是按数据标识,而是按位置标识。即每一个bit即是一个标识,而bit上的0/1表示true/false。所以2字节一共可以使用16个标识位,目前使用了8个。
本例中访问标志在 ~b8~b92。
0 1 2 3 4 5 6 7 8 9 a b c d e f
b: #### #### #### #### 0021
按照位现实的思路,他就代表具有public和super标志,用位来表示就是:00010001=0x0021。
类、父类和接口集合
访问标志之后的6个字节用来标记类、父类和接口集合。
0 1 2 3 4 5 6 7 8 9 a b c d e f
b: #### #### #### #### #### 0003 0004 0000
~ba~bb:0x0003表示类对应的数据在常量池#3位置。#3是一个class,并且指向#17——"example/classLifecicle/SimpleClass",这个字符串就是当前类的全限定名。
~bc~bd:0x0004表示父类对应常量池#4的值,JVM解析常量池后可以还原出父类的全限定名为"java/lang/Object"。
接口能多重继承,因此是一个集合,结构为:2字节表示接口个数,后续每2字节的记录常量池的索引位置。这里 ~be~bf 的数据为0x0000,表示没有任何接口。
fields 字段集合
随后是表示字段的集合,一般用来记录成员变量。
{ //字段集合
length:1,//字段个数,2byte
info:[{
accessFlag:'PUBLIC', //访问标志,2byte
name:constantPool.info[4].id //名称,引用常量池数据,2byte
description:constantPool.info[5].id //描述,引用常量池数据,2byte
attributes:{length:0,[]} //属性集合
}]
}
如上图,字段集合首先2个字节表示有多少个字段。然后是一个列表,列表中每个元素分为4个部分,前三个部分每个2个字节。第一个部分是字段访问标志、第二个部分是字段名称的常量池索引,第三个部分是描述(类型)的常量池索引,第四个部分是属性。属性也是一个不定长度的集合。
字段的访问标志和类一样,也是2个字节按位标识:
名称 | 标志值(0x) | 位(bit) | 描述 |
PUBLIC | 0x0001 | 0000000000000001 | 字段是否为public |
PRIVATE | 0x0002 | 0000000000000010 | 字段是否为private |
PROTECTED | 0x0004 | 0000000000000100 | 字段是否为protected |
STATIC | 0x0008 | 0000000000001000 | 字段是否为static |
FINAL | 0x0010 | 0000000000010000 | 字段是否为final |
VOLATILE | 0x0040 | 0000000000100000 | 字段是否为volatile |
TRANSIENT | 0x0080 | 0000000001000000 | 字段是否为transient |
SYNTHETIC | 0x1000 | 0001000000000000 | 字段是否由编译器自动产生 |
ENUM | 0x4000 | 0100000000000000 | 字段是否为enum |
字段的描述是用一个简单的符号来表示字段的类型:
表示字符 | 含义 | 标识字符 | 含义 |
B | byte字节类型 | J | long长整型 |
C | char字符类型 | S | short短整型 |
D | double双精度浮点 | Z | boolean布尔型 |
F | float单精度浮点 | V | void类型 |
I | int整型 | L | 对象引用类型 |
本例中 ~c0~c9就是整个字段集合,
0 1 2 3 4 5 6 7 8 9 a b c d e f
c: 0001 0002 0005 0006 0000
~c0~c1:表示集合个数,这里只有1个字段。
~c2~c3:0x0002表示第一个字段的访问标志,这里表示私有成员。
~c4~c5:0x0005表示第一个字段的名称,这里索引常量池的#5,值为"i"。
~c6~c7:0x0006表示第一个字段的描述,这里索引常量池的#6,值为"I",表示是一个int。
~c8~c9:0x0000表示第一个字段的属性,这里的0表示没有属性。
根据上面的内容,我们可以还原出这个字段的结构:private int i。
如果定义了值,例如:private int i = 123。会存在一个名为ConstantValue的常量属性,指向常量池的一个值。
方法集合与属性集合
字段解析完毕之后就是方法。方法集合的结构和字段集合的结构几乎一样,也是先有一个列表个数,然后列表的每个元素分成访问标志、名称索引、描述、属性4个部分:
{ //方法集合
length:1,//方法个数,2byte
info:[{
accessFlag:'PUBLIC', //访问标志,2byte
name:constantPool.info[4].id //名称,引用常量池数据,2byte
description:constantPool.info[5].id //描述,引用常量池数据,2byte
attributes:{length:0,[]} //属性集合
}]
}
方法的访问标志:
名称 | 标志值(0x) | 位(bit) | 描述 |
PUBLIC | 0x0001 | 0000000000000001 | 方法是否为public |
PRIVATE | 0x0002 | 0000000000000010 | 方法是否为private |
PROTECTED | 0x0004 | 0000000000000100 | 方法是否为protected |
STATIC | 0x0008 | 0000000000001000 | 方法是否为static |
FINAL | 0x0010 | 0000000000010000 | 方法是否为final |
BRIDGE | 0x0040 | 0000000000100000 | 方法是否由编译器生成的桥接方法 |
VARARGS | 0x0080 | 0000000001000000 | 方法是否不定参数 |
NATIVE | 0x0100 | 0000000100000000 | 方法是否为native |
ABSTRACT | 0x0400 | 0000010000000000 | 方法是否为abstract |
STRICTFP | 0x0800 | 0000100000000000 | 方法是否为strictfp |
SYNTHETIC | 0x1000 | 0001000000000000 | 方法是否由编译器自动产生 |
方法集合从 ~ca 开始:
0 1 2 3 4 5 6 7 8 9 a b c d e f
c: #### #### #### #### #### 0002 0001 0007
d: 0008 0001 0009
~ca~cb:0x0002表示有2个方法。
~cc~cd:0x0001表示第一个方法的访问标志为public。
~ce~cf:0x0007表示第一个方法的名称在常量池#7位置——"<init>"。
~d0~d1:0x0008表示第一个方法的描述在常量池#8位置——"()V",它表示一个没有参数传入的方法,返回一个void。
~d2~d3:0x0001表示第一个方法有一个属性。随后的 ~d4~d5 的0x0009表示属性的名称索引,值为"Code"
前面已经多次提到属性的概念。在字节码中属性也是一个集合结构。目前JVM规范已经预定义21个属性,常见的有"Code"、"ConstantValue"、"Deprecated"等。每一个属性都需要通过一个索引指向常量池的UTF8类型表示属性名称。除了预定义的属性之外,用户还可以添加自己的属性。一个标准的属性结构如下:
名称 | 字节数 | 描述 | 数量 |
name_index | 2 | 常量池表示属性名称的索引 | 1 |
length | 4 | 属性信息的长度 (单位字节) | 1 |
info | length | 属性内容 | length |
每一种属性的属性内容都有自己的结构,下面"Code"属性的结构:
名称 | 字节数 | 描述 | 数量 |
max_stack | 2 | 最大堆栈数 | 1 |
max_locals | 2 | 最大本地槽数 | 1 |
code_length | 4 | 指令集数 | 1 |
code | code_length | 代码内容 | code_length |
exceptions_table_length | 2 | 异常输出表数 | 1 |
exceptions_table | 异常输出表 | ||
attributes_count | 2 | 属性个数 | 1 |
attributes | 属性内容 |
回到本文的例子:
0 1 2 3 4 5 6 7 8 9 a b c d e f
d: #### #### 0009 0000 001d 0001 0001 0000
e: 0005 2ab7 0001 b100 0000 0100 0a00 0000
f: 0600 0100 0000 03
从d4开始就是"<init>"方法的"Code"属性,前面已经说了d4~d5表示这个属性的常量池索引。
~d6~d9:4个字节表示属性的长度,0x0000001d表示属性长度为29——后续29个字节都为该属性的内容。
~da~db:0x0001表示最大堆栈数为1。
~dc~dd: 0x0001表示最大本地槽(本地内存)为1。
~de~e1: 0x00000005表示方法的指令集长度为5。
~e2~e6:'2a b7 00 01 b1'5个字节就是该方法的指令集。指令集是用于JVM堆栈计算的代码,每个代码用1个字节表示。所以JVM一共可以包含0x00~0xff 255个指令,目前已经 使用200多个(指令对照表)。
- 0x2a=>aload_0:表示从本地内存的第一个引用类型参数放到栈顶。
- 0xb7=>invokespecial:表示执行一个方法,方法会用栈顶的引用数据作为参数,调用后续2字节数据指定的常量池方法。
- 0x0001=>是invokespecial的参数,表示引用常量池#1位置的方法。查询常量池可知#2指定的是"<init>"构造方法。
- 0xb1=>return,表示从当前方法退出。
~e7~e8:0x0000表示异常列表,0代表"<init>"方法没有任何异常处理。
~e9~e10:0x0001表示"Code"中还包含一个属性。
~eb~ec:0x000a表示属性名称的常量池索引#10="LineNumberTable"。这个属性用于表示字节码与Java源码之间的关系。"LineNumberTable"是一个非必须属性,可以通过javac -g:[none|lines]命令来控制是否输出该属性。
~ed~f0:0x00000006表示"LineNumberTable"属性所占的长度,后续的6个字节即为该属性的内容。"LineNumberTable"属性也有自己的格式,主要分为2部分,首先是开头2个字节表示行号列表的长度。然后4个字节一组,前2字节表示字节码行号,后2字节表示Java源码行号。
~f1~f2:0x0001表示"LineNumberTable"的行对应列表只有一行。
~f3~f6:0x0000 0003表示字节码的0行对应Java代码的第3行。
到这里为止第一个"<init>"方法的内容解析完毕。~ca~f6 都是这个方法相关的信息。
从 ~f7 开始是第二个方法:
0 1 2 3 4 5 6 7 8 9 a b c d e f
f: 0600 0100 0000 0300 0100 0b00 0c00 0100
10: 0900 0000 1d00 0100 0100 0000 052a b400
11: 02ac 0000 0001 000a 0000 0006 0001 0000
12: 0006
~f7~f8:方法访问标志,0x0001=>PUBLIC。
~f9~fa:方法名称常量池索引,0x000b=>#11=>"get"。
~fb~fc:方法描述符常量池索引,0x000c=>#12=>"()I",表示一个无参数,返回整数类型的方法。
~fd~fe:0x0001表示方法有一个属性。
~ff~100:表示该属性的命名常量池索引,0x0009=>#9=>"Code"。
~101~104:"Code"属性长度,0x00001d=>29字节。
~105~106:最大堆栈数,0x0001=>最大堆栈为1。
~107~109:最大本地缓存的个数,0x0001=>最大数为1。
~10a~10c:指令集长度,0x000005=>一共无个字节的指令。
~10d~111:指令集。0x2a=>aload_0,压入本地内存引用。0xb4=>getfield,使用栈顶的实例数据获取域数据,并将结果压入栈顶。0x0002=>getfield指令的参数,表示引用常量池#2指向的域——private int i。0xac=>ireturen,退出当前方法并返回栈顶的整型数据。
~112~113:异常列表,0x0000表示没有异常列表。
~114~115:属性数,0x0001表示有一个属性。
~116~117:属性名称索引,0x000a=>#10=>"LineNumberTable"。
~118~11b:属性字节长度,0x00000006=>6个字节。
~11c~11d:"LineNumberTable"属性的列表长度,0x0001=>一行。
~11e~121:源码对应行号,0x0000 0006,字节码第0行对应Java源码第6行。
get方法解析完毕,整个方法集合也解析完毕。
类属性
最后几个字节是类的属性描述。
0 1 2 3 4 5 6 7 8 9 a b c d e f
12: #### 0001 000d 0000 0002 000e 0a
~122~123:0x0001表示有一个属性。
~124~125:属性的常量索引,0x000d=>#13=>"SourceFile"。这个属性就是"SourceFIle"。
~126~129:属性的长度,0x00000002=>2个字节。
~12a~12b:属性内容,"SourceFIle"表示指向的源码文件名称,0x000e=>#14=>"SimpleClass.java"。
异常列表和异常属性
异常列表
在前面的例子中并没有说明字节码如何解析和处理异常。在Java源码中 try-catch-finally 的结构用来处理异常。将前面的例子加上一段异常处理:
package example.classLifecicle;
public class SimpleClass{
private int i;
public int get() throws RuntimeException {
int local = 1;
try {
local = 10;
}catch(Exception e) {
local = 0;
}finally {
i = local;
}
return local;
}
}
下面这一段是编译后get方法的字节码片段,从 ~1c 开始:
0 1 2 3 4 5 6 7 8 9 a b c d e f
1: #### #### #### #### #### #### 0001 000c
2: 000d 0002 000a
~1c~1d:方法的访问权限,0x0001 => PUBLIC。
~1e~1f:方法的名称常量池索引,0x000c=>#12=>"get"。
~20~21:方法的描述常量池索引,0x00d=>#13=>"()I"。
~22~23:方法的属性集合长度,0x0002表示有2个集合。
~24~25:方法第一个属性的名称,0x000a=>#10=>"Code"。所以这是一个Code属性,按照Code的规范解析。
0 1 2 3 4 5 6 7 8 9 a b c d e f
2: #### #### 000a 0000 0091 0002 0004 0000
3: 0022 043c 100a 3c2a 1bb5 0002 a700 164d
4: 033c 2a1b b500 02a7 000b 4e2a 1bb5 0002
5: 2dbf 1bac
~26~29:Code属性占用的字节数,0x00000091=>145字节。
~2a~2b:最大堆栈,2个。
~2c~2d:最大本地变量个数,4个。
~2e~31:指令集占用的字节数:0x00000022=>34。
~32~53:34个字节的指令集。
- ~32~34 共2个指令,对应try之前的源码—— int local = 1 :
行号 | 偏移位 | 字节码 | 指令 | 说明 |
1 | ~32 | 0x04 | iconst_1 | 栈顶压入整数1 |
2 | ~33 | 0x3c | istore_1 | 栈顶元素写入本地内存[1] |
- ~34~3e 对应try 括号之间的源码:
行号 | 偏移位 | 字节码 | 指令 | 说明 |
2 | ~34 | 0x10 | bipush | 栈顶压入1字节整数 |
-- | ~35 | 0x0a | 10 | bipush指令的参数 |
4 | ~36 | 0x3c | istore_1 | 栈顶整数存入本地存储[1] |
5 | ~37 | 0x2a | aload_0 | 本地存储[0]的引用压入栈顶 |
6 | ~38 | 0x1b | iload_1 | 本地存储[1]的整数压入栈顶 |
7 | ~39 | 0xb5 | putfield | 更新字段数据 |
-- | ~3a~3b | 0x0002 | #2 | putfield的参数。(#2,10,this) |
10 | ~3c | 0xa7 | goto | |
~3d~3e | 0x0016 | 32行 | goto指令的参数 |
- ~3f~48 对应catch括号之间的源码:
行号 | 偏移位 | 字节码 | 指令 | 说明 |
13 | ~3f | 0x4d | astore_2 | 栈顶引用存入本地存储[2] |
14 | ~40 | 0x3c | iconst_0 | 整数0压入栈顶 |
15 | ~41 | 0x3c | istore_1 | 栈顶整数0存入本地存储[1] |
16 | ~42 | 0x2a | aload_0 | 本地存储[0]引用压入栈顶 |
17 | ~43 | 0x1b | iload_1 | 本地存储[1]整数0压入栈顶 |
18 | ~44 | 0xb5 | putfield | 更新字段数据 |
-- | ~45~46 | 0x0002 | #2 | putfield的参数。(#2,10,this) |
21 | ~47 | 0xa7 | goto | |
-- | ~48~49 | 0x0016 | 32行 | goto指令的参数 |
- ~4a~51 对应finally括号内的代码:
行号 | 偏移位 | 字节码 | 指令 | 说明 |
24 | ~4a | 0x4e | astore_3 | 栈顶引用存入本地存储[3] |
25 | ~4b | 0x2a | aload_0 | 本地存储[0]引用压入栈顶 |
26 | ~4c | 0x1b | iload_1 | 本地存储[1]整数压入栈顶 |
27 | ~4d | 0xb5 | putfield | 本地存储[0]引用压入栈顶 |
-- | ~4e~4f | 0x0002 | #2 | putfield的参数。(#2,?,this) |
30 | ~50 | 0x2d | aload_3 | 本地存储[3]引用压入栈顶 |
31 | ~51 | 0xbf | athrow | 跑出栈顶异常 |
- 最后 ~52~53 就是 return local :
行号 | 偏移位 | 字节码 | 指令 | 说明 |
32 | ~52 | 0x1b | iload_1 | 本地存储[1]整数压入栈顶 |
33 | ~53 | 0xac | ireturn | 返回栈顶的整数 |
0 1 2 3 4 5 6 7 8 9 a b c d e f
5: #### #### 0003 0002 0005 000d 0003 0002
6: 0005 0018 0000 000d 0010 0018 0000
按照前面对Code属性的介绍,~54~55 表示异常列表,这里的值为0x0003,表示异常列表有3行。异常列表的属性结构如下:
{
length:3,// 2byte表示异常列表个数
info:[
{
start_pc: 2 // 拦截异常开始的行号,2byte
end_pc: 5 // 拦截异常结束的行号,2byte
handler_pc: 13 // 异常处理的行号,2byte
catch_type: 3 //异常类型,指向常量池的索引,2byte
}
]
}
~56~6d 都是记录异常列表的结构。
~56~57:拦截异常开始的位置,0x0002=>第2行。
~58~59:拦截异常结束的位置,0x0005=>第5行。
~5a~5b:异常处理的位置,0x000d=>13行。
~5c~5d:异常类型的常量池索引,0x0003=>#3=>"java/lang/Exception"。
对应异常列表结构将 ~56~6d 部分的字节流 还原成一个表:
start_pc | end_pc | handler_pc | catch_type |
2 | 5 | 13 | "java/lang/Exception" |
2 | 5 | 24 | 所有异常 |
13 | 16 | 24 | 所有异常 |
对照前面的指令集,这个表结构就是告诉JVM:
- 如果在字节码2到5行遇到"java/lang/Exception"异常,跳转到13行继续执行。等价于try跳转到catch中。
- 如果在字节码2到5行遇到异常(排除"java/lang/Exception"及其父类的异常),跳转到24行继续执行。等价于遇到Exception之外的异常,直接跳转到finally块去处理。
- 如果在字节码13到16行遇到任何异常,跳转到24行执行。等价于在catch块中遇到异常,跳转到finally块继续执行。
Code属性结合异常列表就完成对所有执行中遇到异常的处理。
异常列表属性之后 ~6e~c0 是LineNumberTable和StackMapTable属性。
0 1 2 3 4 5 6 7 8 9 a b c d e f
6: #### #### #### #### #### #### #### 0002
7: 000b 0000 002a 000a 0000 0005 0002 0007
8: 0005 000b 000a 000c 000d 0008 000e 0009
9: 0010 000b 0015 000c 0018 000b 0020 000d
a: 000e 0000 0015 0003 ff00 0d00 0207 000f
b: 0100 0107 0010 4a07 0011 0700 1200 0000
c: 04
异常属性
get方法除了Code属性外,还有一个Exception属性。他的作用是列举出该方法抛出的可查异常,即方法体throws关键字后面声明的异常。其结构为:
{
exceptionNumber:1, //抛出异常的个数 2byte
exceptionTable[16] //抛出异常的列表,每一个值指向常量池的Class类型,每个元素2byte
}
字节码 ~c1~c4 就是异常属性:
0 1 2 3 4 5 6 7 8 9 a b c d e f
c: ##00 0100 13
~c1~c2:0x0001表示该方法抛出一个异常。
~c3~c4:0x0013表示抛出的异常类型指向常量池#19位置的 Class ,即"java/lang/RuntimeException"。
到此,2进制流的异常处理介绍完毕。
总结
Jvm识别字节码的过程到此介绍完毕,按照这个识别过程可以理解JVM是怎么一步一步解析字节码的。有机会的话可以跳出Java语言在JVM的基础上倒腾自己的语言,Scala、Groovy、Kotlin也正是这样做的。在JSR-292之后,JVM就完全脱离Java成为了一个更加独立且更加生机勃勃的规范体系。
能够理解字节码和JVM的识别过程还可以帮助我们更深层次优化代码。无论Java代码写得再漂亮也要转换成字节码去运行。从字节码层面去看运行的方式,要比从Java源码层面更为透彻。
理解字节码还有一个好处,更容易理解多线程的3个主要特性:原子性、可见性和有序性。比如new Object() 从字节码层面一看就知道不具备原子性,指令重排的问题在字节码层面也是一目了然。
附录
常量池类型
常量表类型 | 标志 | 描述 |
CONSTANT_Utf8 | 1 | UTF-8编码的Unicode字符串 |
CONSTANT_Integer | 3 | int类型的字面值 |
CONSTANT_Float | 4 | float类型的字面值 |
CONSTANT_Long | 5 | long类型的字面值 |
CONSTANT_Double | 6 | double类型的字面值 |
CONSTANT_Class | 7 | 对一个类或接口的符号引用 |
CONSTANT_String | 8 | String类型字面值的引用 |
CONSTANT_Fieldref | 9 | 对一个字段的符号引用 |
CONSTANT_Methodref | 10 | 对一个类中方法的符号引用 |
CONSTANT_InterfaceMethodref | 11 | 对一个接口中方法的符号引用 |
CONSTANT_NameAndType | 12 | 对一个字段或方法的部分符号引用 |
CONSTANT_MethodHandle | 15 | 表示方法的句柄 |
CONSTANT_MethodType | 16 | 标识方法的类型 |
CONSTANT_InvokeDynamic | 18 | 标识一个动态方法调用点 |
格式化的字节码信息附录
Classfile /work/myRepository/MetaSpaceOutError/src/main/java/example/classLifecicle/SimpleClass.class
Last modified Dec 4, 2017; size 300 bytes
MD5 checksum c78b7fb8709a924751d31028768a430d
Compiled from "SimpleClass.java"
public class example.classLifecicle.SimpleClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // example/classLifecicle/SimpleClass.i:I
#3 = Class #17 // example/classLifecicle/SimpleClass
#4 = Class #18 // java/lang/Object
#5 = Utf8 i
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 get
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 SimpleClass.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // i:I
#17 = Utf8 example/classLifecicle/SimpleClass
#18 = Utf8 java/lang/Object
{
public example.classLifecicle.SimpleClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public int get();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field i:I
4: ireturn
LineNumberTable:
line 6: 0
}
SourceFile: "SimpleClass.java"