为什么80%的码农都做不了架构师?>>>   

字节码解析

结构

本位将详细介绍字节码的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 f0: 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,//常量个数,2byteinfo:[{id:"#1“, //索引, 1bytetype:"UTF8", // 类型, 1byteparams:"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~1cUTF8 的字符串长度,0x0001表示自由一个字符。

~1d:表示字符编码,0x69=105,对应的ASCII就是字符"i"。

字节码采用UTF-8缩略编码的方式编码——'\u0001'到'\u007f'的字符串(相当于ASCII 0~127)只用一个字节表示,而'\u0800'到'\uffff'的编码使用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~4fUTF8 ,"get",表示我们代码中的get方法的字面名称。

     0 1  2 3  4 5  6 7  8 9  a b  c d  e f  4: #### #### #### #### #### 0100 0367 6574  

#12~50~55UTF8 ,"()I",表示一个返回整数的方法符号。

     0 1  2 3  4 5  6 7  8 9  a b  c d  e f  5: 0100 0328 2949 

#13~56~62UTF8 ,长度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~75UTF8 ,"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,//字段个数,2byteinfo:[{accessFlag:'PUBLIC', //访问标志,2bytename:constantPool.info[4].id //名称,引用常量池数据,2bytedescription:constantPool.info[5].id //描述,引用常量池数据,2byteattributes:{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,//方法个数,2byteinfo:[{accessFlag:'PUBLIC', //访问标志,2bytename:constantPool.info[4].id //名称,引用常量池数据,2bytedescription:constantPool.info[5].id //描述,引用常量池数据,2byteattributes:{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 f1: #### #### #### #### #### #### 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 f2: #### #### 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 f5: #### #### 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 // 拦截异常开始的行号,2byteend_pc: 5 // 拦截异常结束的行号,2bytehandler_pc: 13 // 异常处理的行号,2bytecatch_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:

  1. 如果在字节码2到5行遇到"java/lang/Exception"异常,跳转到13行继续执行。等价于try跳转到catch中。
  2. 如果在字节码2到5行遇到异常(排除"java/lang/Exception"及其父类的异常),跳转到24行继续执行。等价于遇到Exception之外的异常,直接跳转到finally块去处理。
  3. 如果在字节码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 f6: #### #### #### #### #### #### #### 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, //抛出异常的个数 2byteexceptionTable[16] //抛出异常的列表,每一个值指向常量池的Class类型,每个元素2byte
}

字节码 ~c1~c4 就是异常属性:

     0 1  2 3  4 5  6 7  8 9  a b  c d  e fc: ##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.classLast modified Dec 4, 2017; size 300 bytesMD5 checksum c78b7fb8709a924751d31028768a430dCompiled from "SimpleClass.java"
public class example.classLifecicle.SimpleClassminor version: 0major version: 52flags: 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: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 3: 0public int get();descriptor: ()Iflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: getfield      #2                  // Field i:I4: ireturnLineNumberTable:line 6: 0
}
SourceFile: "SimpleClass.java"

转载于:https://my.oschina.net/chkui/blog/1586388

JVM与字节码——2进制流字节码解析相关推荐

  1. python16进制转10进制_python 字节串及10进制,16进制相关转换

    进行协议解析时,总是会遇到各种各样的数据转换的问题,从二进制到十进制,从字节串到整数等等 整数之间的进制转换: 10进制转16进制: hex(16) ==> 0x10 16进制转10进制: in ...

  2. C# 中2,10,16进制及其ASCII码之间转化

    转载连接: http://blog.csdn.net/pengfeihe0123/article/details/7278221 C# 中2,10,16进制及其ASCII码之间转化           ...

  3. 计算机进制、原码、反码、补码、移码相关知识

    计算机进制.原码.反码.补码相关知识 一:计算机中的数制 二:数制之间的转换 三:无符号数.有符号数 四:二进制原码.反码.补码.移码(增码) 4.1 原码 4.2 反码 4.3 补码 4.4 移码( ...

  4. C语言 枚举,进制,原码,反码,补码,移码,结构体

    该博文为原创文章,未经博主同意不得转载,如同意转载请注明博文出处 本文章博客地址:https://cplusplus.blog.csdn.net/article/details/104294089 枚 ...

  5. python学习-综合练习三(斐波那契数列、阿姆斯特朗数、//和/、十进制转二进制bin、八进制oct、十六进制hex、进制转换源码、python中::和:的区别)

    文章目录 斐波那契数列 阿姆斯特朗数 十进制转二进制bin.八进制oct.十六进制hex 补充进制转换源码 python中::和:的区别 说明:本篇博文的知识点大部分来自 Python3 实例 斐波那 ...

  6. 用bat脚本写一个输入16进制转ascii码的代码

    以下是一个使用 BAT 脚本实现 16 进制转 ASCII 码的代码示例: @echo off set /p hex=请输入16进制字符串: for /l %%i in (0,1,%hex:~0,-2 ...

  7. pythonprint字节按照16进制输出_对python以16进制打印字节数组的方法详解

    对python以16进制打印字节数组的方法详解 一.问题描述 如果直接用print打印bytes的话,有时候会直接显示ascii对应的字符,看起来很蛋疼. 二.运行效果 上面一行是直接用print打印 ...

  8. c语言 bcd码 16进制字符串 原理,ASCII码、HEX、字符、BCD 等等 基础知识思考

    每每遇到这些问题就要想个半天,想不明白还不舒服,今天特别把所想整理下避免以后再次进入思想漩涡!!! 计算机存储和传输都是以字节为单位 1 bit     = 1  二进制数据 1 byte  = 8  ...

  9. 计算机语言有几进制,一个字节由几个二进制位组成(计算机系统有什么两部分组成)...

    一个字节由8个二进制位组成的.字节(Byte /bait/ n. [C])是计算机信息技术用于计量存储容量的一种计量单位,也表示一些计算机编程语言中的数据类型和语言字符.. 在计算机中,一个" ...

最新文章

  1. oracle trunc()截断函数
  2. php如何发起get网络请求,php发送get、post请求的几种方法
  3. 异常处理python要求输入的为英文_python(异常处理)
  4. 8086汇编-实验5(微调)-用栈实现数据交换位置
  5. 机器学习中的数学(二)--梯度下降法
  6. golang sync.map
  7. 好玩的WPF第二弹:电子表字体显示时间+多彩呼吸灯特效按钮
  8. Matlab安装minGW
  9. Android Binder机制介绍
  10. 腾讯地图基于 WebGL实现自定义栅格图层踩坑实录
  11. Windows 10升级无法选择保留个人文件、设置问题解决
  12. 谷歌关闭中国地区音乐搜索服务与产品设计
  13. 手把手教你制作炫酷的PCB板3D效果图
  14. spring boot 报错:extShutdownHook ...was destroying!
  15. Java微信公众平台开发_03_消息管理之被动回复消息
  16. 苹果硬改手机底层udid无限新机技术
  17. 职场教育小课堂:信息时代的管理方法
  18. GO Strings-and-runes
  19. 已解决org.springframework.beans.factory.UnsatisfiedDependencyException org.springframework.beans.factor
  20. 使用国标流媒体服务器查看监控摄像头视频流如何正确使用UDP及TCP协议?

热门文章

  1. 微信扫码登陆PC端(Vue)(小程序)
  2. 菜鸟入门帖:笔记本常见接口功能解析
  3. 计算机的复数英语怎么读,巧记scissors复数英语怎么读 – 附音标
  4. C++ Opengl 绘制3D字体源码
  5. jQuery 用each() 添加click事件
  6. 【汇正财经】怎么样开户?
  7. Python实现录屏
  8. 用node实现cors跨域
  9. 简易计算器:支持两个整数的加、减、乘、除运算
  10. [转]Pinyin4j入门教程