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)

描述

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:

  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_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"

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏容器云生态

awk-grep-sed简单使用总结(正则表达式的应用)

正则表达式: 匹配一组字符: #[ns]a.\.xls  //[]用于限定字符;“.”用于匹配任意字符; \.用于转义"." 匹配到s/na*.xls  [n...

2779
来自专栏Pythonista

python内建函数

abs()函数返回数字(可为普通型、长整型或浮点型)的绝对值。如果给出复数,返回值就是该复数的模。例如:

1771
来自专栏codingforever

经典算法巡礼(二) -- 排序之选择排序

选择排序,如冒泡排序一样,从名字中即可大概猜测其排序的原理。其工作原理就是从未排序的数组中选出最大(小)的元素,将其放置至数组的首(尾)部,重复此过程直至没有未...

401
来自专栏数据科学与人工智能

【Python环境】12道 Python面试题总结

1、Python是如何进行内存管理的? Python的内存管理主要有三种机制:引用计数机制、垃圾回收机制和内存池机制。 a. 引用计数 当给一个对象分配一个新名...

2545
来自专栏猿人谷

不用加减乘除做加法

题目:写一个函数,求两个整数之和,要求在函数体内不得使用+、-、×、÷四则运算符号。 分析: 第一步:不考虑进位对每一位相加。0加0、1加1的结果都是0,0加1...

2197
来自专栏null的专栏

挑战数据结构与算法面试题——统计上排数在下排出现的次数

题目来源“数据结构与算法面试题80道”。在此给出我的解法,如你有更好的解法,欢迎留言。 ? 分析: 本题应该是一个确定的问题,即上排的是个数是题目中给定的...

3246
来自专栏Python

Python常见数据结构整理 Python常见数据结构整理

Python常见数据结构整理 Python中常见的数据结构可以统称为容器(container)。序列(如列表和元组)、映射(如字典)以及集合(set)是三类主要...

1967
来自专栏进击的君君的前端之路

面向对象、this

1243
来自专栏haifeiWu与他朋友们的专栏

Kotlin委托

Kotlin中有委托,这个C#中也有,不过对于学Java的童鞋来说,这是什么鬼啊,到底是干什么用的… 在委托模式中,当有两个对象参与处理同一个请求是,接受请求的...

2033
来自专栏CVer

排序算法 | 冒泡排序(含C++/Python代码实现)

排序算法,就是如何使得记录按照要求排列的方法。排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面。排序算法有很多,本文将介绍最经典的排序算法:冒泡排序...

1322

扫码关注云+社区

领取腾讯云代金券