JVM与字节码——2进制流字节码解析

字节码解析

结构

本位将详细介绍字节码的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~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)描述
PUBLIC0x00010000000000000001对应public类型的类
FINAL   0x00100000000000010000对应类的final声明
SUPER0x00200000000000100000标识JVM的invokespecial新语义
INTERFACE0x02000000001000000000接口标志
ABSTRACT0x04000000010000000000抽象类标志
SYNTHETIC0x10000001000000000000标识这个类并非用户代码产生
ANNOTATION0x20000010000000000000标识这是一个注解
ENUM0x40000100000000000000标识这是一个枚举

访问标志不是按数据标识,而是按位置标识。即每一个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)描述
PUBLIC0x00010000000000000001字段是否为public
PRIVATE 0x00020000000000000010字段是否为private
PROTECTED0x00040000000000000100字段是否为protected
STATIC0x00080000000000001000字段是否为static
FINAL0x00100000000000010000字段是否为final
VOLATILE0x00400000000000100000字段是否为volatile
TRANSIENT0x00800000000001000000字段是否为transient
SYNTHETIC0x10000001000000000000字段是否由编译器自动产生
ENUM0x40000100000000000000字段是否为enum

字段的描述是用一个简单的符号来表示字段的类型:

表示字符含义标识字符 含义
Bbyte字节类型Jlong长整型
Cchar字符类型Sshort短整型
Ddouble双精度浮点Zboolean布尔型
Ffloat单精度浮点Vvoid类型
Iint整型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)描述
PUBLIC0x00010000000000000001方法是否为public
PRIVATE 0x00020000000000000010方法是否为private
PROTECTED0x00040000000000000100方法是否为protected
STATIC0x00080000000000001000方法是否为static
FINAL0x00100000000000010000方法是否为final
BRIDGE0x00400000000000100000方法是否由编译器生成的桥接方法
VARARGS0x00800000000001000000方法是否不定参数
NATIVE0x01000000000100000000方法是否为native
ABSTRACT0x04000000010000000000方法是否为abstract
STRICTFP0x08000000100000000000方法是否为strictfp
SYNTHETIC0x10000001000000000000方法是否由编译器自动产生

方法集合从 ~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_index2常量池表示属性名称的索引1
length4属性信息的长度 (单位字节)1
infolength属性内容length

每一种属性的属性内容都有自己的结构,下面"Code"属性的结构:

名称字节数描述数量
max_stack2最大堆栈数1
max_locals2最大本地槽数1
code_length4指令集数1
codecode_length代码内容code_length
exceptions_table_length2异常输出表数1
exceptions_table 异常输出表 
attributes_count2属性个数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~320x04iconst_1栈顶压入整数1
2~330x3cistore_1栈顶元素写入本地内存[1]
  • ~34~3e 对应try 括号之间的源码:
行号偏移位字节码指令说明
2~340x10bipush栈顶压入1字节整数
--~350x0a10bipush指令的参数
4~360x3cistore_1栈顶整数存入本地存储[1]
5~370x2aaload_0本地存储[0]的引用压入栈顶
6~380x1biload_1本地存储[1]的整数压入栈顶
7~390xb5putfield更新字段数据
--~3a~3b0x0002#2putfield的参数。(#2,10,this)
10~3c0xa7goto 
 ~3d~3e0x001632行goto指令的参数
  • ~3f~48 对应catch括号之间的源码:
行号偏移位字节码指令说明
13~3f0x4dastore_2栈顶引用存入本地存储[2]
14~400x3ciconst_0整数0压入栈顶
15~410x3cistore_1栈顶整数0存入本地存储[1]
16~420x2aaload_0本地存储[0]引用压入栈顶
17~430x1biload_1本地存储[1]整数0压入栈顶
18~440xb5putfield更新字段数据
--~45~460x0002#2putfield的参数。(#2,10,this)
21~470xa7goto 
--~48~490x001632行goto指令的参数
  • ~4a~51 对应finally括号内的代码:
行号偏移位字节码指令说明
24~4a0x4eastore_3栈顶引用存入本地存储[3]
25~4b0x2aaload_0本地存储[0]引用压入栈顶
26~4c0x1biload_1本地存储[1]整数压入栈顶
27~4d0xb5putfield本地存储[0]引用压入栈顶
--~4e~4f0x0002#2putfield的参数。(#2,?,this)
30~500x2daload_3本地存储[3]引用压入栈顶
31~510xbfathrow跑出栈顶异常
  • 最后 ~52~53 就是  return local  :
行号偏移位字节码指令说明
32~520x1biload_1本地存储[1]整数压入栈顶
33~530xacireturn返回栈顶的整数
     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_pcend_pchandler_pccatch_type
2513"java/lang/Exception"
2524所有异常
131624所有异常

对照前面的指令集,这个表结构就是告诉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 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_Utf81UTF-8编码的Unicode字符串
CONSTANT_Integer3int类型的字面值
CONSTANT_Float4float类型的字面值
CONSTANT_Long5long类型的字面值
CONSTANT_Double6double类型的字面值
CONSTANT_Class7对一个类或接口的符号引用
CONSTANT_String8String类型字面值的引用
CONSTANT_Fieldref9对一个字段的符号引用
CONSTANT_Methodref10对一个类中方法的符号引用
CONSTANT_InterfaceMethodref11对一个接口中方法的符号引用
CONSTANT_NameAndType12对一个字段或方法的部分符号引用
CONSTANT_MethodHandle15表示方法的句柄
CONSTANT_MethodType16标识方法的类型
CONSTANT_InvokeDynamic18标识一个动态方法调用点
格式化的字节码信息附录
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"

 

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值