本文详细介绍了如何使用javap查看java方法中的字节码、以及各种字节码的含义,并且配以完善的案例,一步步,从头到尾带领大家翻译javap的输出。在文末还附有JVM字节码指令集表。

本文不适合没有JVM基础的初学者,看文章之前希望你有如下基本技能:了解JVM的一些基本概念,比如什么是符号引用?什么是字面量?了解class文件的基本结构,了解基于栈的JVM方法执行的栈基本结构!关于class文件的结构可以看:Java的 Class(类)文件结构详解。

看完本文,你可能获得如下知识:JVM字节码的入门、如何查看和分析字节码的执行、加深对JVM中的栈的理解,基于字节码层面,或许还能为你解开一些原始的疑惑,比如:为什么finally块中的语句总会被执行?

1 字节码概述

字节码是一套设计用来在Java虚拟机中执行的高度优化的指令集。Java字节码对于虚拟机,就好像汇编语言对于计算机,属于基本执行指令,或者说是Java代码的在JVM中的最小执行单元。JVM只关心字节码,而不关心源文件是属于在哪个平台用哪种语言编写的,字节码是实现JVM平台无关性和语言无关性的基石。

JVM字节码指令由一个字节长度的、代表着某种特定操作含义的数字称为操作码,Opcode),以及跟随其后的零到多个代表此操作所需参数(称为操作数,Operands)构成。

所谓一个字节的长度,也就是8位二进制数字,也就是两位十六进制数字。

每一个字节码还拥有它对应的助记符形式。顾名思义,助记符就是帮助我们查看、理解字节码的含义的符号,一般我们看字节码操作都通过是看助记符来分辨的。但是实际的执行运行并不存在助记符这些东西,都是根据字节码的值来执行。

由于操作码的长度为一个字节,因此操作码(指令)最多不超过256条;

对于操作数长度超过了一个字节的情况(取决于操作码):由于Class文件格式放弃了编译后代码的操作数长度对齐,所以,当JVM处理超过一个字节长度的数据时,需要在运行时从字节中重建出具体数据的结构,如16位二进制数据需要(byte1 << 8)|byte2操作(Big-Endian 顺序存储——即高位在前的字节序)。

2 基于javap的字节码解析

2.1 javap介绍

如果直接打开class文件,只能查看数字形式的字节码。

我们可以使用javap工具,反编译class文件,将数字类型的字节码转换成我们能看懂的格式,进而快捷的查看一个java类的常量池、方法表、code属性(方法字节码)、局部变量表、异常表等等信息。在这里,我认为javap反编译之后输出的数据,并不是传统意义上的汇编指令,只是The Java Virtual Machine Instruction Set(Java 虚拟机指令集)的一种方便人们理解的表现形式。

javap的格式为: javap < options > < classes >

其中options选项如下:

-version

版本信息,其实是当前javap所在jdk的版本信息,不是cass在哪个jdk下生成的。

-l

输出行号和本地变量表

-c

对代码进行反汇编,主要是针对方法的代码

-v -verbose

不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用到的常量池等详细信息。

-pubic

仅显示公共类和成员

-protected

显示受保护的/公共类和成员

-package

显示程序包/受保护的/公共类 和成员 (默认)

-p -private

显示所有类和成员

-s

输出内部类型签名

-sysinfo

显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列)

-constants

显示静态最终常量

-classpath

指定查找用户类文件的位置

-bootclasspath

覆盖引导类文件的位置

一般常用的是-v -l -c三个选项,-v展示的最全面。官方javap介绍地址为:javap

2.2 javap案例

2.2.1 源码类

源码类使用在介绍class文件结构时候使用的源码类:Java的 Class(类)文件结构详解。方便连续学习。这里再贴出来一下:

public class ClassFile {

public static final String J = "2222222";

private int k;

public int getK() {

return k;

}

public void setK(int k) throws Exception {

try {

this.k = k;

} catch (IllegalStateException e) {

e.printStackTrace();

} finally {

}

}

public static void main(String[] args) {

}

}

2.2.2 javap输出解析

对源码的class文件,使用javap -v ClassFile.class指令,得到如下信息(灰色的文字是后来自己加的注释)。

对于初学者,应该一行行的仔细查看其含义,对于重要的部分单独拿出来讲解,比如方法字节码部分。

Classfile /J:/Idea/jvm/target/classes/com/ikang/JVM/classfile/ClassFile.class //生成该class文件的源文件名称和路径

Last modified 2020-4-8; size 960 bytes //上一次修改时间和大小

MD5 checksum fcd8ef238722b6dab50c0495a9673a1f //MD5信息

Compiled from "ClassFile.java" //编译自ClassFile.java类

public class com.ikang.JVM.classfile.ClassFile

minor version: 0 //小版本号

major version: 52 //大版本号

flags: ACC_PUBLIC, ACC_SUPER //类访问标记, ACC_PUBLIC表示public, ACC_SUPER表示允许使用invokespecial字节码指令的新语意

//class常量池开始,可以看出有46个常量

Constant pool:

//第1个常量是CONSTANT_Methodref_info类型,表示类中方法的符号引用,具有两个索引属性,分别指向第6个和第37个常量,其最终值为java/lang/Object."":()V

//这明显是< init >方法 的符号引用,编译时自动生成的。

#1 = Methodref #6.#37 // java/lang/Object."":()V

//第2个常量是CONSTANT_Fieldref_info类型,表示类中字段的符号引用,具有两个索引属性,分别指向第5个和第38个常量 其最终值为com/ikang/JVM/classfile/ClassFile.k:I

//这明显是k字段的符号引用

#2 = Fieldref #5.#38 // com/ikang/JVM/classfile/ClassFile.k:I

//第3个常量是CONSTANT_Class_info类型,表示类或接口的符号引用,具有一个索引属性,指向第39个常量 其最终值为java/lang/IllegalStateException

//这明显是引入的IllegalStateException异常类的符号引用

#3 = Class #39 // java/lang/IllegalStateException

//第4个常量是CONSTANT_Methodref_info类型,表示类中方法的符号引用,具有两个索引属性,分别指向第3个和第40个常量,其最终值为java/lang/IllegalStateException.printStackTrace:()V

//这明显是e.printStackTrace()方法的符号引用

#4 = Methodref #3.#40 // java/lang/IllegalStateException.printStackTrace:()V

//第5个常量是CONSTANT_Class_info类型,表示类或接口的符号引用,具有一个索引属性,指向第41个常量 其最终值为com/ikang/JVM/classfile/ClassFile

//这明显是本类(ClassFile类)的符号引用

#5 = Class #41 // com/ikang/JVM/classfile/ClassFile

//第6个常量是CONSTANT_Class_info类型,表示类或接口的符号引用,具有一个索引属性,指向第41个常量 其最终值为java/lang/Object

//这明显是本类的父类Object的符号引用

#6 = Class #42 // java/lang/Object

//第7个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是J

//这明显储存的常量J的名称的字面值

#7 = Utf8 J

//第7个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是Ljava/lang/String;

//这明显表示一个对象类型的字段描述符的字面值,String类型

#8 = Utf8 Ljava/lang/String;

//第9个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是ConstantValue

//这明显表示常量字段的ConstantValue属性的名字的字符串字面值

#9 = Utf8 ConstantValue

//第10个常量是CONSTANT_String_info类型,表示字符串类型字面量,用来表示类型,具有一个指向具体字面值的索引43, 这里的具体值是2222222

//这明显是常量 J的字面量,因为是字符串类型

#10 = String #43 // 2222222

//第11个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 k

//这明显储存的变量J的名称的字面值

#11 = Utf8 k

//第12个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 I

//这明显表示一个int类型的字段描述符的字面值

#12 = Utf8 I

//第13个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是

//这明显表示方法名称的字面值

#13 = Utf8

//第14个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 ()V

//这明显表示一个返回值为void类型、没有参数的方法描述符的字面值,那么可以表示构造方法的方法描述符的字面值

#14 = Utf8 ()V

//第15个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 Code

//这明显表示方法中的Code属性的名称的字面值

#15 = Utf8 Code

//第16个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 k

//这明显表示方法的Code属性中的LineNumberTable属性的名称的字面值

#16 = Utf8 LineNumberTable

//第17个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 k

//这明显表示方法的Code属性中的LocalVariableTable属性的名称的字面值

#17 = Utf8 LocalVariableTable

//第18个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 this

//这明显表示方法的局部变量this的名称的字面值

#18 = Utf8 this

//第19个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 Lcom/ikang/JVM/classfile/ClassFile;

//这明显表示一个对象类型的字段描述符的字面值,ClassFile类型

#19 = Utf8 Lcom/ikang/JVM/classfile/ClassFile;

//第20个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 getK

//这明显表示getK方法名称的字面值

#20 = Utf8 getK

//第21个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 ()I

//这明显表示一个返回值为int类型、没有参数的方法描述符的字面值,那么可以表示getK方法的方法描述符的字面值

#21 = Utf8 ()I

//第22个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 setK

//这明显表示setK方法名称的字面值

#22 = Utf8 setK

//第23个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 (I)V

//这明显表示一个返回值为void类型、参数为int类型的方法描述符的字面值,那么可以表示setK方法的方法描述符的字面值

#23 = Utf8 (I)V

//第24个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 e

//这个e是什么呢?实际上是表示参数的名称的字面值

#24 = Utf8 e

//第25个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是Ljava/lang/IllegalStateException;

//这明显表示一个对象类型的字段描述符的字面值,IllegalStateException类型

#25 = Utf8 Ljava/lang/IllegalStateException;

//第26个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是Ljava/lang/IllegalStateException;

//这明显表示方法的Code属性中的StackMapTable属性的名称的字面值

#26 = Utf8 StackMapTable

//第27个常量是CONSTANT_Class_info类型,表示类或接口的符号引用,具有一个索引属性,指向第39个常量 其最终值为java/lang/IllegalStateException

//这明显是IllegalStateException类的符号引用

#27 = Class #39 // java/lang/IllegalStateException

//第28个常量是CONSTANT_Class_info类型,表示类或接口的符号引用,具有一个索引属性,指向第44个常量 其最终值为java/lang/Throwable

//这明显是Exception的父类Throwable类的符号引用

#28 = Class #44 // java/lang/Throwable

//第29个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是Exceptions

//加了个s,肯定不是类名字面量了,实际上它是方法表中的Exceptions异常表属性的名字 字符串字面量

#29 = Utf8 Exceptions

//第30个常量是CONSTANT_Class_info类型,表示类或接口的符号引用,具有一个索引属性,指向第45个常量 其最终值为java/lang/Exception

//这明显是Exception类的符号引用

#30 = Class #45 // java/lang/Exception

//第31个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是main

//这明显是main方法的方法名字 字符串字面量

#31 = Utf8 main

//第32个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 ([Ljava/lang/String;)V

//这明显表示一个返回值为void类型、参数为String一维数组类型的方法描述符的字面值,那么可以表示main方法的方法描述符的字面值

#32 = Utf8 ([Ljava/lang/String;)V

//第33个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 ([Ljava/lang/String;)V

//这明显表示main方法的参数名的字符串字面量

#33 = Utf8 args

//第34个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 [Ljava/lang/String;

//这明显表示一个对象类型的字段描述符的字面值,String[]类型

#34 = Utf8 [Ljava/lang/String;

//第35个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 SourceFile

//这明显表示SourceFile属性的名字的字面值

#35 = Utf8 SourceFile

//第36个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 ClassFile.java

//这明显表示SourceFile属性的具体值的字面值

#36 = Utf8 ClassFile.java

//第37个常量是CONSTANT_NameAndType_info类型,表示字段或方法的部分符号引用,还持有两个索引,分别指向13和14个字符串,这里的最终值是 "":()V,我们回过头去看第13和14个字符串,它们储存的是具体的字面值。

//这个常量被我们的第1个常量持有,即表示< init >方法的部分符号引用

#37 = NameAndType #13:#14 // "":()V

//第38个常量是CONSTANT_NameAndType_info类型,表示字段或方法的部分符号引用,还持有两个索引,分别指向11和12个字符串,这里的最终值是 k:I,我们回过头去看第11和12个字符串,它们储存的是具体的字面值。

//这个常量被我们的第2个常量持有,即表示k字段的部分符号引用

#38 = NameAndType #11:#12 // k:I

//第39个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 java/lang/IllegalStateException

//这个常量被我们的第3、27个常量持有,这明显表示储存IllegalStateException类的符号引用的具体字面值

#39 = Utf8 java/lang/IllegalStateException

//第40个常量是CONSTANT_NameAndType_info类型,表示字段或方法的部分符号引用,还持有两个索引,分别指向46和14个字符串,这里的最终值是 k:I,我们去看第46和14个字符串,它们储存的是具体的字面值。

//这个常量被我们的第4个常量持有,即表示printStackTrace方法的部分符号引用

#40 = NameAndType #46:#14 // printStackTrace:()V

//第41个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 com/ikang/JVM/classfile/ClassFile

//这个常量被我们的第5个常量持有,这明显表示储存本类(ClassFile类)的符号引用的具体字面值

#41 = Utf8 com/ikang/JVM/classfile/ClassFile

//第42个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 com/ikang/JVM/classfile/ClassFile

//这个常量被我们的第6个常量持有,这明显表示储存本Object类的符号引用的具体字面值

#42 = Utf8 java/lang/Object

//第43个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 2222222

//这个常量被我们的第10个常量持有,这明显表示储存常量J的具体值

#43 = Utf8 2222222

//第44个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 java/lang/Throwable

//这个常量被我们的第28个常量持有,这明显表示储存Exception的父类Throwable类的符号引用具体值

#44 = Utf8 java/lang/Throwable

//第45个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 java/lang/Throwable

//这个常量被我们的第30个常量持有,这明显表示储存Exception类的符号引用具体值

#45 = Utf8 java/lang/Exception

//第46个常量是CONSTANT_utf8_info类型,表示UTF-8编码的字符串,用来存储具体的字符串字面值 这里的值是 printStackTrace

//这个常量被我们的第40个常量持有,这明显表示printStackTrace方法名的具体值

#46 = Utf8 printStackTrace

//class常量池结束,后面是字段表和方法表

{

//常量字段J

public static final java.lang.String J;

//常量字段J的字段描述符 String类型

descriptor: Ljava/lang/String;

//常量字段J的访问标记 public static final

flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

//常量字段J的额外ConstantValue属性,该属性中又包括常量字段的类型和具体值,这里可以看出来,对于常量字段在编译时就确定了值,对于常量字段,在类加载的准备阶段就已经通过ConstantValue初始化为最终值了,对常量字段的引用不会导致类进入“初始化”阶段。

ConstantValue: String 2222222

/*方法表的默认构造方法开始*/

public com.ikang.JVM.classfile.ClassFile();

//方法描述符

descriptor: ()V

//方法访问标记

flags: ACC_PUBLIC

//方法Code属性

Code:

//栈最大深度1,局部变量最大数量1,参数1

stack=1, locals=1, args_size=1

/*下面是方法字节码,对于方法字节码,在后面提出来重点说明*/

0: aload_0

1: invokespecial #1 // Method java/lang/Object."":()V

4: return

//行号表, 用于描述Java源代码行号与字节码行号(字节码偏移量)之间的对应关系,每一行第一个数字对应代码行数,第二个数字对应前面code中字节码字节码指令左边的数字。一行可以对应多个字节码指令。

LineNumberTable:

line 4: 0

//局部变量表, start+length表示这个变量在字节码中的生命周期起始和结束的偏移位置,Name就是变量的名字,slot就是这个变量在局部变量表中的槽位(槽位可复用,从0开始),name就是变量名称,Signatur局部变量的字段类型描述符;

//下面的含义就是名叫this的局部变量的字节码生命周期从头0到结尾5(不包括),占用第一个槽位(索引0),变量是ClassFile类型。

LocalVariableTable:

Start Length Slot Name Signature

0 5 0 this Lcom/ikang/JVM/classfile/ClassFile;

/*方法表的默认构造方法 结束*/

/*方法表的getk方法 开始*/

public int getK();

//方法描述符,很简单,表示返回值为int ,参数为空

descriptor: ()I

//访问标记 public

flags: ACC_PUBLIC

//Code属性,包括方法字节码,行号表,局部变量表

Code:

//栈最大深度1,局部变量最大数量1,参数1

stack=1, locals=1, args_size=1

/*下面是方法字节码,对于方法字节码,在后面提出来重点说明*/

0: aload_0

1: getfield #2 // Field k:I

4: ireturn

//行号表

LineNumberTable:

line 10: 0

//局部变量表,只有一个this变量

LocalVariableTable:

Start Length Slot Name Signature

0 5 0 this Lcom/ikang/JVM/classfile/ClassFile;

/*方法表的getk方法 结束*/

/*方法表的setK方法 开始*/

public void setK(int) throws java.lang.Exception;

//方法描述符,很简单,表示返回值为void ,参数为int

descriptor: (I)V

//访问标记 public

flags: ACC_PUBLIC

//Code属性,包括方法字节码,行号表,局部变量表,该方法还包括异常表、栈图

Code:

//栈最大深度2,局部变量最大数量4,参数2

stack=2, locals=4, args_size=2

/*下面是方法字节码,对于方法字节码,在后面提出来重点说明*/

0: aload_0

1: iload_1

2: putfield #2 // Field k:I

5: goto 19

8: astore_2

9: aload_2

10: invokevirtual #4 // Method java/lang/IllegalStateException.printStackTrace:()V

13: goto 19

16: astore_3

17: aload_3

18: athrow

19: return

//异常表属性, 在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的,而是采用异常表来完成的。异常表是由try-catch语句生成的。具体结构在class文件结构那篇文章处有介绍。

Exception table:

from to target type

//第一行表示 0~5(不包括) 行出现的 IllegalStateException 异常,直接跳转到 8(astore_2) 行;java代码层面就是:如果try语句块中出现属于IllegalStateException或其子类的异常,则转到catch语句块处理。

0 5 8 Class java/lang/IllegalStateException

//第二行表示 0~5(不包括) 行出现的 其余的所有异常, 直接跳转到 16(astore_3) 行,java代码层面就是:如果try语句块中出现不属于IllegalStateException或其子类的异常,则转到finally语句块处理。从这里可以看出,finally关键字的语义也是由异常表实现的。

0 5 16 any

//第三行表示 8~13(不包括) 行出现的 其余的所有异常, 直接跳转到 16(astore_3) 行;java代码层面就是:如果catch语句块中出现任何异常,则转到finally语句块处理。

8 13 16 any

//行号表

LineNumberTable:

line 16: 0

line 20: 5

line 17: 8

line 18: 9

line 20: 13

line 19: 16

line 21: 19

//局部变量表,有3个局部变量

LocalVariableTable:

Start Length Slot Name Signature

9 4 2 e Ljava/lang/IllegalStateException;

0 20 0 this Lcom/ikang/JVM/classfile/ClassFile;

0 20 1 k I

//栈图,该属性不包含运行时所需的信息,仅仅用于用于加快Class文件的类型检验。

StackMapTable: number_of_entries = 3

frame_type = 72 /* same_locals_1_stack_item */

stack = [ class java/lang/IllegalStateException ]

frame_type = 71 /* same_locals_1_stack_item */

stack = [ class java/lang/Throwable ]

frame_type = 2 /* same */

//列举异常,表示一个方法可能抛出的异常,通常是由方法的throws 关键字指定的,这和Code属性中的异常表不一样。

Exceptions:

throws java.lang.Exception

/*方法表的setK方法 结束*/

/*方法表的main方法 开始*/

public static void main(java.lang.String[]);

//方法描述符,很简单,表示返回值为void ,参数为String[]

descriptor: ([Ljava/lang/String;)V

//访问标记,public static

flags: ACC_PUBLIC, ACC_STATIC

//code属性

Code:

stack=0, locals=1, args_size=1

//由于没有代码,因此只有return一个字节码,表示方法的结束.

0: return

LineNumberTable:

line 24: 0

LocalVariableTable:

Start Length Slot Name Signature

0 1 0 args [Ljava/lang/String;

}

//记录class文件的源文件 ClassFile.java

SourceFile: "ClassFile.java"

2.2.2.1 构造方法的字节码解析

0: aload_0

1: invokespecial #1 // Method java/lang/Object."":()V

4: return

左边的数字表示字节码偏移量(从0开始),字节码的偏移量和前面的字节码长度有关系,例如aload_0是第一个字节码,自然偏移量是0,它本身占一个字节,因此 invokespecial就从索引1开始,invokespecial本身占据一个字节,但是接收一个两个字节的无符号数参数,因此整个invokespecial指令占据3个字节,故后续的return指令位于偏移量4处。

在最开始时,局部变量表中具有一个变量this。局部变量表第0项为this引用,表示当前对象。在Java 中, 对千所有的非静态函数调用, 为了能顺利访问this对象,都会将对象的引用放置在局部变量表第0 个槽位,如下图:

执行第一条指令,aload_0,表示把局部变量第 1 个引用型局部变量推到操作数栈,即把this对象引用压入操作数栈的栈顶(操作数栈相当于虚拟机的工作区,主要用于字节码的执行——大多数指令都要从这里先压入原始数据,然后弹出数据,执行运算,然后把结果压回操作数栈,最后把结果赋给局部变量表的变量),如下图:

执行第二条指令,invokespecial, 表示对栈顶对象的私有、构造、父类方法进行调用,这些方法的特点是调用目标在编译时就确定了,对这些方法的调用又称为“解析”。该指令接收一个2个字节长度的无符号参数,用于构建一个当前类的运行时常量池的索引值,该索引所指向的运行时常量池项应当是一个方法的符号引用,表示将执行栈顶对象对应的该方法。

javap已经帮我们算出来了,该索引就是值为1的索引处的常量项,我们回去看看第一项常量,是< init >方法的符号引用。即调用实例初始化方法(< init >)来完成对象的初始化,就是this指定的对象的调用< init >方法完成初始化。

第二条指令执行完,即该类对象被初始化完毕,执行第三条指令,return,表示方法结束,并从当前方法返回 void,并清空栈空间数据包括操作数栈和局部变量表。

从上面的三条指令可以看出来,在无参构造方法调用了< init >方法对对象进行初始化, < init >方法用于将对象字段的零值初始化为指定值。

2.2.2.2 getK方法的字节码解析

0: aload_0

1: getfield #2 // Field k:I

4: ireturn

在最开始时,同样局部变量表中具有一个变量this,操作数栈为空。

执行第一个指令,aload_0,表示把 局部变量第1个引用型局部变量推到操作数栈顶 ,即把this对象引用压入操作数栈的栈顶。

执行第二个指令,getfield,表示 获取指定对象的字段值,并将其值压入栈顶 。该指令接收一个2个字节长度的无符号参数,用于构建一个当前类的运行时常量池的索引值,该索引所指向的运行时常量池项应当是一个字段的符号引用,指令执行后,该字段的值将被取出,并压入到操作数栈顶。很明显,这里指向第二项常量,我们回去看看第二项常量,的确是k字段 的符号引用。第二个指令执行完毕后如下图:

执行第三个指令,ireturn,表示结束方法并返回一个int 类型数据。很明显是将操作数栈的栈顶值返回(对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作)。

2.2.2.3 getKsetK方法的字节码

0: aload_0

1: iload_1

2: putfield #2 // Field k:I

5: goto 19

8: astore_2

9: aload_2

10: invokevirtual #4 // Method java/lang/IllegalStateException.printStackTrace:()V

13: goto 19

16: astore_3

17: aload_3

18: athrow

19: return

//异常表属性, 在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的,而是采用异常表来完成的。异常表是由try-catch语句生成的。具体结构在class文件结构处有介绍。

Exception table:

from to target type

//第一行表示 0~5(不包括) 行出现的 IllegalStateException 异常,直接跳转到 8(astore_2) 行;java代码层面就是:如果try语句块中出现属于IllegalStateException或其子类的异常,则转到catch语句块处理。

0 5 8 Class java/lang/IllegalStateException

//第二行表示 0~5(不包括) 行出现的 其余的所有异常, 直接跳转到 16(astore_3) 行,java代码层面就是:如果try语句块中出现不属于IllegalStateException或其子类的异常,则转到finally语句块处理。从这里可以看出,finally关键字的语义也是由异常表实现的。

0 5 16 any

//第三行表示 8~13(不包括)行出现的 其余的所有异常, 直接跳转到 16(astore_3) 行;java代码层面就是:如果catch语句块中出现任何异常,则转到finally语句块处理。

8 13 16 any

该方法的字节码明显变多了,主要加了一些异常处理逻辑。不过一个一个看,也很简单。

执行第1个指令,aload_0,表示把局部变量第1个引用型局部变量推到操作数栈顶,即把this对象引用压入操作数栈的栈顶。

执行第2个指令,iload_1,表示把局部变量第2个int型局部变量推到操作数栈顶,即把k字段的值压入操作数栈的栈顶。

执行第3个指令,putfield,表示设置对象字段的值。该指令接收一个2个字节长度的无符号参数,用于构建一个当前类的运行时常量池的索引值,该索引所指向的运行时常量池项应当是一个字段的符号引用,指令执行后,操作数栈中的该字段所属的对象this和具体的值,将被弹出栈。putfield的参数指向常量池第二个常量,明显表示k字段。

执行第4个指令,goto,表示无条件分支跳转。 该指令接收一个2个字节长度的有符号参数,用于构建一个16 位有符号的分支偏移量。指令执行后,程序将会转到这个goto 指令之后的,由上述偏移量确定的目标地址上继续执行。这个目标地址必须处于goto 指令所在的方法之中。

很明显,后面的偏移量是19,这说明为k赋值之后,就可以执行return了。因为本代码finally块中没有定义代码,实际上如果finally中有代码,那么将会在每一个goto之前插入finally的字节码,这也从字节码的层面保证了finally语句块必须执行!

执行第5个指令,astore_2,表示。把栈顶引用型数值存入第3个局部变量表位置。

执行第6个指令,aload_2,表示把局部变量第3个引用型局部变量推到操作数栈。

执行第7个指令,invokevirtual,表示调用实例方法,依据实例的类型进行方法分派。该指令接收一个2个字节长度的无符号参数,用于构建一个当前类的运行时常量池的索引值,该索引所指向的运行时常量池项应当是一个方法的符号引用,这里指向第四个常量,我们返回查看常量池,发现是printStackTrace方法的符号引用。

实际上,上面表格部分的字节码,就是catch块中的内容。我们根据异常表,当发生IllegalStateException异常,将会跳转到astore_2,执行catch中的内容。

查看局部变量表可知,实际上astore_2是将IllegalStateException异常引用存放了局部变量表的第三个位置。

第6、7个指令实际上就是在调用printStackTrace方法。

执行第8个指令,goto,表示无条件分支跳转。这是第二个goto指令,在代码逻辑层面:表示catch执行完毕,finally语句块执行完毕(本finally中没有代码),方法结束。

第9个指令,astore_3, 表示把栈顶引用型数值存入第4个局部变量表位置。能够执行到这个指令,根据异常表,说明catch中出现了异常或者出现不属于IllegalStateException或其子类的异常,将该异常类型,放到第四个局部变量表位置。我们去查看局部变量表,并没有第四个异常变量,这说明这一段字节码的逻辑不一定会执行,在编译时还不确定该异常的类型。

第10个指令,aload_3, 表示把局部变量第4个引用型局部变量推到操作数栈。即将上面的异常,放到操作数栈。但是在该指令之前还会执行finally中的字节码。

第11个指令,athrow, 表示将栈顶的异常抛出。即将上面的未能使用catch捕获的异常抛出,并结束方法。

第12个指令,return,作为方法结束的标志。

到此,setk方法结束。

补充:

如果在finally中添加一个输出语句:System.out.println(11);那么字节码将会如下:

我们可以看到,finally中的字节码被插入到每一个可能的分支的最后,代码层面就表示:无论如何,finally中代码都将被执行,这是jvm在编译时为我们做出的字节码级别的保证。

3 jclasslib替代javap

实际上,现在通过jclasslib工具可以完全替代javap命令,jclasslib可用于用于打开class文件,而且是可视化的,效果更好一些。jclasslib还提供了修改jar包中的Class文件的API。

为什么先介绍比较麻烦的javap?因为javap是Java官方自带的,先了解原生的工具之后,再来使用jclasslib,我们将会更加的得心应手。

jclasslib的github地址:jclasslib。

3.1 idea安装jclasslib插件

使用 ALT+CTRL+S 打开setting

选择plugins(插件),搜索jclasslib,然后安装,重启

选择要打开的class文件,点击上面的view,点击show bytecode with jclasslib,在右侧就会出现可视化界面

右侧的可视化界面,可以看出来还是很简单的,这里就不介绍了。

3.2 直接安装jclasslib

当然有些开发者没有使用idea, github上这里也可以直接安装jclasslib。

下载地址:https://github.com/ingokegel/jclasslib/releases

安装好之后,将class文件放入jclasslib中,可以看到,界面和idea中的界面差不多。

如果由于某些神奇的原因,github上展示无法下载,可以使用本人提供的链接:jclasslib。

3.3 修改class文件字节码案例

3.3.1 准备对比输出

因为修改class文件的代码使用到了同名的类,为了不引起混淆,将原代码的类名改为ClassFile1,然后main方法添加如下代码:

public static void main(String[] args) {

System.out.println(J);

}

就是简单的打印常量J的值运行输出:2222222。

拿到class文件和源码放到一个路径下面。

在包路径开始处,运行cmd,尝试使用命令行带包执行class文件:

可见还是输出为2222222

3.3.2 修改class文件

我们此次作简单的修改,将输出2222222改成3333。

我们的jclasslib已经提供了修改class文件的API。在jclasslib的安装目录中找到lib目录,将下面的jar包拷贝到项目中:

编写代码:

public class ModifyClass {

public static void main(String[] args) throws Exception {

FileInputStream fis = null;

try {

//自己的class文件路径

String filePath = "J:\\Idea\\jvm\\src\\main\\java\\com\\ikang\\JVM\\classfile\\ClassFile1.class";

fis = new FileInputStream(filePath);

DataInput di = new DataInputStream(fis);

ClassFile cf = new ClassFile();

cf.read(di);

CPInfo[] infos = cf.getConstantPool();

//找到常量池46个位置,CONSTANT_Utf-8_info所以这里要用这个

ConstantUtf8Info uInfo = (ConstantUtf8Info) infos[46];

//设置新值

uInfo.setBytes("3333".getBytes());

//替换回去

infos[46] = uInfo;

cf.setConstantPool(infos);

File f = new File(filePath);

ClassFileWriter.writeToFile(f, cf);

} finally {

if (fis != null) {

fis.close();

}

}

}

}

为什么找到46个位置呢,因为要修改的J常量的“2222222”字面量值就是存在第46个常量中:

运行之后,再次使用java命令执行原来的class文件:

结果输出3333,修改成功。

4 附:字节码指令表

字节码指令根据功能、属性不同,可以分为11大类。下面附上字节码指令的分类,用于简单、临时查看,字节码指令的详细介绍,还需要查看官网的介绍。

4.1 Constants 常量相关

十进制

操作码

助记符

含义

00

0x00

nop

什么都不做

01

0x01

aconst_null

把 null 推到操作数栈

02

0x02

iconst_m1

把 int 常量 –1 推到操作数栈

03

0x03

iconst_0

把 int 常量 0 推到操作数栈

04

0x04

iconst_1

把 int 常量 1 推到操作数栈

05

0x05

iconst_2

把 int 常量 2 推到操作数栈

06

0x06

iconst_3

把 int 常量 3 推到操作数栈

07

0x07

iconst_4

把 int 常量 4 推到操作数栈

08

0x08

iconst_5

把 int 常量 5 推到操作数栈

09

0x09

lconst_0

把 long 常量 0 推到操作数栈

10

0x0A

lconst_1

把 long 常量 1 推到操作数栈

11

0x0B

fconst_0

把 float 常量 0 推到操作数栈

12

0x0C

fconst_1

把 float 常量 1 推到操作数栈

13

0x0D

fconst_2

把 float 常量 2 推到操作数栈

14

0x0E

dconst_0

把 double 常量 0 推到操作数栈

15

0x0F

dconst_1

把 double 常量 1 推到操作数栈

16

0x10

bipush

把单字节常量(-128~127)推到操作数栈

17

0x11

sipush

把 short 常量(-32768~32767)推到操作数栈

18

0x12

ldc

把常量池中的int,float,String型常量取出并推到操作数栈顶

19

0x13

ldc_w

把常量池中的int,float,String型常量取出并推到操作数栈顶(宽索引)

20

0x14

ldc2_w

把常量池中的long,double型常量取出并推到操作数栈顶(宽索引)

4.2 Loads 加载相关

十进制

操作码

助记符

含义

21

0x15

iload

把 int 型局部变量推到操作数栈

22

0x16

lload

把 long 型局部变量推到操作数栈

23

0x17

fload

把 float 型局部变量推到操作数栈

24

0x18

dload

把 double 型局部变量推到操作数栈

25

0x19

aload

把引用型局部变量推到操作数栈

26

0x1A

iload_0

把局部变量第 1 个 int 型局部变量推到操作数栈

27

0x1B

iload_1

把局部变量第 2 个 int 型局部变量推到操作数栈

28

0x1C

iload_2

把局部变量第 3 个 int 型局部变量推到操作数栈

29

0x1D

iload_3

把局部变量第 4 个 int 型局部变量推到操作数栈

30

0x1E

lload_0

把局部变量第 1 个 long 型局部变量推到操作数栈

31

0x1F

lload_1

把局部变量第 2 个 long 型局部变量推到操作数栈

32

0x20

lload_2

把局部变量第 3 个 long 型局部变量推到操作数栈

33

0x21

lload_3

把局部变量第 4 个 long 型局部变量推到操作数栈

34

0x22

fload_0

把局部变量第 1 个 float 型局部变量推到操作数栈

35

0x23

fload_1

把局部变量第 2 个 float 型局部变量推到操作数栈

36

0x24

fload_2

把局部变量第 3 个 float 型局部变量推到操作数栈

37

0x25

fload_3

把局部变量第 4 个 float 型局部变量推到操作数栈

38

0x26

dload_0

把局部变量第 1 个 double 型局部变量推到操作数栈

39

0x27

dload_1

把局部变量第 2 个 double 型局部变量推到操作数栈

40

0x28

dload_2

把局部变量第 3 个 double 型局部变量推到操作数栈

41

0x29

dload_3

把局部变量第 4 个 double 型局部变量推到操作数栈

42

0x2A

aload_0

把局部变量第 1 个引用型局部变量推到操作数栈

43

0x2B

aload_1

把局部变量第 2 个引用型局部变量推到操作数栈

44

0x2C

aload_2

把局部变量第 3 个引用型局部变量推到操作数栈

45

0x2D

aload_3

把局部变量第 4 个引用 型局部变量推到操作数栈

46

0x2E

iaload

把 int 型数组指定索引的值推到操作数栈

47

0x2F

laload

把 long 型数组指定索引的值推到操作数栈

48

0x30

faload

把 float 型数组指定索引的值推到操作数栈

49

0x31

daload

把 double 型数组指定索引的值推到操作数栈

50

0x32

aaload

把引用型数组指定索引的值推到操作数栈

51

0x33

baload

把 boolean或byte型数组指定索引的值推到操作数栈

52

0x34

caload

把 char 型数组指定索引的值推到操作数栈

53

0x35

saload

把 short 型数组指定索引的值推到操作数栈

4.3 Store 存储相关

十进制

操作码

助记符

含义

54

0x36

istore

把栈顶 int 型数值存入指定局部变量

55

0x37

lstore

把栈顶 long 型数值存入指定局部变量

56

0x38

fstore

把栈顶 float 型数值存入指定局部变量

57

0x39

dstore

把栈顶 double 型数值存入指定局部变量

58

0x3A

astore

把栈顶引用型数值存入指定局部变量

59

0x3B

istore_0

把栈顶 int 型数值存入第 1 个局部变量

60

0x3C

istore_1

把栈顶 int 型数值存入第 2 个局部变量

61

0x3D

istore_2

把栈顶 int 型数值存入第 3 个局部变量

62

0x3E

istore_3

把栈顶 int 型数值存入第 4 个局部变量

63

0x3F

lstore_0

把栈顶 long 型数值存入第 1 个局部变量

64

0x40

lstore_1

把栈顶 long 型数值存入第 2 个局部变量

65

0x41

lstore_2

把栈顶 long 型数值存入第 3 个局部变量

66

0x42

lstore_3

把栈顶 long 型数值存入第 4 个局部变量

67

0x43

fstore_0

把栈顶 float 型数值存入第 1 个局部变量

68

0x44

fstore_1

把栈顶 float 型数值存入第 2 个局部变量

69

0x45

fstore_2

把栈顶 float 型数值存入第 3 个局部变量

70

0x46

fstore_3

把栈顶 float 型数值存入第 4 个局部变量

71

0x47

dstore_0

把栈顶 double 型数值存入第 1 个局部变量

72

0x48

dstore_1

把栈顶 double 型数值存入第 2 个局部变量

73

0x49

dstore_2

把栈顶 double 型数值存入第 3 个局部变量

74

0x4A

dstore_3

把栈顶 double 型数值存入第 4 个局部变量

75

0x4B

astore_0

把栈顶 引用 型数值存入第 1 个局部变量

76

0x4C

astore_1

把栈顶 引用 型数值存入第 2 个局部变量

77

0x4D

astore_2

把栈顶 引用 型数值存入第 3 个局部变量

78

0x4E

astore_3

把栈顶 引用 型数值存入第 4 个局部变量

79

0x4F

iastore

把栈顶 int 型数值存入数组指定索引位置

80

0x50

lastore

把栈顶 long 型数值存入数组指定索引位置

81

0x51

fastore

把栈顶 float 型数值存入数组指定索引位置

82

0x52

dastore

把栈顶 double 型数值存入数组指定索引位置

83

0x53

aastore

把栈顶 引用 型数值存入数组指定索引位置

84

0x54

bastore

把栈顶 boolean or byte 型数值存入数组指定索引位置

85

0x55

castore

把栈顶 char 型数值存入数组指定索引位置

86

0x56

sastore

把栈顶 short 型数值存入数组指定索引位置

4.4 Stack 栈相关

十进制

操作码

助记符

含义

87

0x57

pop

把栈顶数值弹出(非long,double数值)

88

0x58

pop2

把栈顶的一个long或double值弹出,或弹出2个其他类型数值

89

0x59

dup

复制栈顶数值并把数值入栈

90

0x5A

dup_x1

复制栈顶数值并将两个复制值压入栈顶

91

0x5B

dup_x2

复制栈顶数值并将三个(或两个)复制值压入栈顶

92

0x5C

dup2

复制栈顶一个(long 或double 类型的)或两个(其它)数值并将复制值压入栈顶

93

0x5D

dup2_x1

dup_x1 指令的双倍版本

94

0x5E

dup2_x2

dup_x2 指令的双倍版本

95

0x5F

swap

把栈顶端的两个数的值交换(数值不能是long 或double 类型< td >的)

4.5 Math 运算相关

Java 虚拟机在处理浮点数运算时,不会抛出任何运行时异常,当一个操作产生溢出时,将会使用有符号的无穷大来表示,如果某个操作结果没有明确的数学定义的话,将会使用 NaN 值来表示。所有使用 NaN 值作为操作数的算术操作,结果都会返回 NaN。

十进制

操作码

助记符

含义

96

0x60

iadd

把栈顶两个 int 型数值相加并将结果入栈

97

0x61

ladd

把栈顶两个 long 型数值相加并将结果入栈

98

0x62

fadd

把栈顶两个 float 型数值相加并将结果入栈

99

0x63

dadd

把栈顶两个 double 型数值相加并将结果入栈

100

0x64

isub

把栈顶两个 int 型数值相减并将结果入栈

101

0x65

lsub

把栈顶两个 long 型数值相减并将结果入栈

102

0x66

fsub

把栈顶两个 float 型数值相减并将结果入栈

103

0x67

dsub

把栈顶两个 double 型数值相减并将结果入栈

104

0x68

imul

把栈顶两个 int 型数值相乘并将结果入栈

105

0x69

lmul

把栈顶两个 long 型数值相乘并将结果入栈

106

0x6A

fmul

把栈顶两个 float 型数值相乘并将结果入栈

107

0x6B

dmul

把栈顶两个 double 型数值相乘并将结果入栈

108

0x6C

idiv

把栈顶两个 int 型数值相除并将结果入栈

109

0x6D

ldiv

把栈顶两个 long 型数值相除并将结果入栈

110

0x6E

fdiv

把栈顶两个 float 型数值相除并将结果入栈

111

0x6F

ddiv

把栈顶两个 double 型数值相除并将结果入栈

112

0x70

irem

把栈顶两个 int 型数值模运算并将结果入栈

113

0x71

lrem

把栈顶两个 long 型数值模运算并将结果入栈

114

0x72

frem

把栈顶两个 float 型数值模运算并将结果入栈

115

0x73

drem

把栈顶两个 double 型数值模运算并将结果入栈

116

0x74

ineg

把栈顶 int 型数值取负并将结果入栈

117

0x75

lneg

把栈顶 long 型数值取负并将结果入栈

118

0x76

fneg

把栈顶 float 型数值取负并将结果入栈

119

0x77

dneg

把栈顶 double 型数值取负并将结果入栈

120

0x78

ishl

把 int 型数左移指定位数并将结果入栈

121

0x79

lshl

把 long 型数左移指定位数并将结果入栈

122

0x7A

ishr

把 int 型数右移指定位数并将结果入栈(有符号)

123

0x7B

lshr

把 long 型数右移指定位数并将结果入栈(有符号)

124

0x7C

iushr

把 int 型数右移指定位数并将结果入栈(无符号)

125

0x7D

lushr

把 long 型数右移指定位数并将结果入栈(无符号)

126

0x7E

iand

把栈顶两个 int 型数值 按位与 并将结果入栈

127

0x7F

land

把栈顶两个 long 型数值 按位与 并将结果入栈

128

0x80

ior

把栈顶两个 int 型数值 按位或 并将结果入栈

129

0x81

lor

把栈顶两个 long 型数值 按或与 并将结果入栈

130

0x82

ixor

把栈顶两个 int 型数值 按位异或 并将结果入栈

131

0x83

lxor

把栈顶两个 long 型数值 按位异或 并将结果入栈

132

0x84

iinc

把指定 int 型增加指定值

4.6 Conversions 转换相关

类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显示类型转换操作。

Java 虚拟机直接支持(即转换时无需显示的转换指令)小范围类型向大范围类型的安全转换,但在处理窄化类型转换时,必须显式使用转换指令来完成。

十进制

操作码

助记符

含义

133

0x85

i2l

把栈顶 int 强转 long 并入栈

134

0x86

i2f

把栈顶 int 强转 float 并入栈

135

0x87

i2d

把栈顶 int 强转 double 并入栈

136

0x88

l2i

把栈顶 long 强转 int 并入栈

137

0x89

l2f

把栈顶 long 强转 float 并入栈

138

0x8A

l2d

把栈顶 long 强转 double 并入栈

139

0x8B

f2i

把栈顶 float 强转 int 并入栈

140

0x8C

f2l

把栈顶 float 强转 long 并入栈

141

0x8D

f2d

把栈顶 float 强转 double 并入栈

142

0x8E

d2i

把栈顶 double 强转 int 并入栈

143

0x8F

d2l

把栈顶 double 强转 long 并入栈

144

0x90

d2f

把栈顶 double 强转 float 并入栈

145

0x91

i2b

把栈顶 int 强转 byte 并入栈

146

0x92

i2c

把栈顶 int 强转 char 并入栈

147

0x93

i2s

把栈顶 int 强转 short 并入栈

4.7 Comparisons 比较相关

十进制

操作码

助记符

含义

148

0x94

lcmp

比较栈顶两long 型数值大小,并将结果(1,0,-1)压入栈顶

149

0x95

fcmpl

比较栈顶两float 型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为“NaN”时,将-1 压入栈顶

150

0x96

fcmpg

比较栈顶两float 型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为“NaN”时,将1 压入栈顶

151

0x97

dcmpl

比较栈顶两double 型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为“NaN”时,将-1 压入栈顶

152

0x98

dcmpg

比较栈顶两double 型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为“NaN”时,将1 压入栈顶

153

0x99

ifeq

当栈顶 int 型数值等于0时,跳转

154

0x9A

ifne

当栈顶 int 型数值不等于0时,跳转

155

0x9B

iflt

当栈顶 int 型数值小于0时,跳转

156

0x9C

ifge

当栈顶 int 型数值大于等于0时,跳转

157

0x9D

ifgt

当栈顶 int 型数值大于0时,跳转

158

0x9E

ifle

当栈顶 int 型数值小于等于0时,跳转

159

0x9F

if_icmpeq

比较栈顶两个 int 型数值,等于0时,跳转

160

0xA0

if_icmpne

比较栈顶两个 int 型数值,不等于0时,跳转

161

0xA1

if_icmplt

比较栈顶两个 int 型数值,小于0时,跳转

162

0xA2

if_icmpge

比较栈顶两个 int 型数值,大于等于0时,跳转

163

0xA3

if_icmpgt

比较栈顶两个 int 型数值,大于0时,跳转

164

0xA4

if_icmple

比较栈顶两个 int 型数值,小于等于0时,跳转

165

0xA5

if_acmpeq

比较栈顶两个 引用 型数值,相等时跳转

166

0xA6

if_acmpne

比较栈顶两个 引用 型数值,不相等时跳转

4.8 Control 控制相关

控制转移指令可以让 Java 虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,从概念模型上理解,可以认为控制转移指令就是在有条件或无条件地修改 PC 寄存器的值。

十进制

操作码

助记符

含义

167

0xA7

goto

无条件分支跳转

168

0xA8

jsr

跳转至指定16 位offset(bit) 位置,并将jsr 下一条指令地址压入栈顶

169

0xA9

ret

返回至局部变量指定的index 的指令位置(一般与jsr,jsr_w联合使用)

170

0xAA

tableswitch

用于switch 条件跳转,case 值连续(可变长度指令)

171

0xAB

lookupswitch

用于switch 条件跳转,case 值不连续(可变长度指令)

172

0xAC

ireturn

结束方法,并返回一个int 类型数据

173

0xAD

lreturn

从当前方法返回 long

174

0xAE

freturn

从当前方法返回 float

175

0xAF

dreturn

从当前方法返回 double

176

0xB0

areturn

从当前方法返回 对象引用

177

0xB1

return

从当前方法返回 void

4.9 references 引用、方法、异常、同步相关

十进制

操作码

助记符

含义

178

0xB2

getstatic

获取指定类的静态域,并将其值压入栈顶

179

0xB3

putstatic

为类的静态域赋值

180

0xB4

getfield

获取指定类的实例域(对象的字段值),并将其值压入栈顶

181

0xB5

putfield

为指定的类的实例域赋值

182

0xB6

invokevirtual

调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),是Java语言中最常见的方法分派方式。

183

0xB7

invokespecial

调用一些需要特殊处理的实例方法,包括实例初始化方法()、私有方法和父类方法。这三类方法的调用对象在编译时就可以确定。

184

0xB8

invokestatic

调用静态方法

185

0xB9

invokeinterface

调用接口方法调,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。

186

0xBA

invokedynamic

调用动态链接方法(该指令是指令是Java SE 7 中新加入的)。用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

187

0xBB

new

创建一个对象,并将其引用值压入栈顶

188

0xBC

newarray

创建一个指定原始类型(如int、float、char……)的数组,并将其引用值压入栈顶

189

0xBD

anewarray

创建一个引用型(如类,接口,数组)的数组,并将其引用值压入栈顶

190

0xBE

arraylength

获得数组的长度值并压入栈顶

191

0xBF

athrow

将栈顶的异常直接抛出。Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现,并且,在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的,而是采用异常表来完成的。

192

0xC0

checkcast

检验类型转换,检验未通过将抛出ClassCastException

193

0xC1

instanceof

检验对象是否是指定的类的实例,如果是将1 压入栈顶,否则将0 压入栈顶

194

0xC2

monitorenter

获取对象的monitor,用于同步块或同步方法

195

0xC3

monitorexit

释放对象的monitor,用于同步块或同步方法

Java 虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。

**方法级的同步是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。**虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 方法标志得知一个方法是否声明为同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个  同步方法所持有的管程将在异常抛到同步方法之外时自动释放。

同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义

编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都必须执行其对应的monitorexit指令,而无论这个方法是正常结束还是异常结束。

4.10 Extended 扩展相关

十进制

操作码

助记符

含义

196

0xC4

wide

扩展访问局部变量表的索引宽度

197

0xC5

multianewarray

创建指定类型和指定维度的多维数组(执行该指令时,操作栈中必须包含各维度的长度值),并将其引用值压入栈顶

198

0xC6

ifnull

为 null 时跳转

199

0xC7

ifnonnull

非 null 时跳转

200

0xC8

goto_w

无条件跳转(宽索引)

201

0xC9

jsr_w

跳转指定32bit偏移位置,并将jsr_w下一条指令地址入栈

4.11 Reserved 保留指令

十进制

操作码

助记符

含义

202

0xCA

breakpoint

调试时的断点

254

0xFE

impdep1

用于在特定硬件中使用的语言后门

255

0xFF

impdep2

用于在特定硬件中使用的语言后门

5 参考和学习

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

本文同步分享在 博客“L-Java”(CSDN)。

如有侵权,请联系 support@oschina.cn 删除。

本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

Java指令全集_Java的JVM字节码指令集详解相关推荐

  1. [三] java虚拟机 JVM字节码 指令集 bytecode 操作码 指令分类用法 助记符

    说明,本文的目的在于从宏观逻辑上介绍清楚绝大多数的字节码指令的含义以及分类 只要认真阅读本文必然能够对字节码指令集有所了解 如果需要了解清楚每一个指令的具体详尽用法,请参阅虚拟机规范 指令简介 计算机 ...

  2. JVM字节码指令集大全及其介绍

    Java是怎么跨平台的 我们上计算机课的时候老师讲过:"计算机只能识别0和1,所以我们写的程序要经过编译器翻译成0和1组成的二进制格式计算机才能执行".我们编译后产生的.class ...

  3. 【深入理解JVM】JVM字节码指令集

    Java 虚拟机的指令由一个字节长度的.代表着某种特定操作含义的操作码(Opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(Operands)所构成.虚拟机中许多指令并不包含操作数,只有一 ...

  4. 1 字节的 utf-8 序列的字节 1 无效_字节码文件结构详解

    点击上方" Java资料站 ",选择"标星公众号" 优质文章,第一时间送达 陈建源  |  作者 urlify.cn/INFrUr  |  来源 "一 ...

  5. java 字符串倍_java字符串拼接与性能分析详解

    假设有一个字符串,我们将对这个字符串做大量循环拼接操作,使用"+"的话将得到最低的性能.但是究竟这个性能有多差?如果我们同时也把StringBuffer,StringBuilder ...

  6. java 静态 编译_Java中的动态和静态编译实例详解

    Java中的动态和静态编译实例详解 首先,我们来说说动态和静态编译的问题. Q: java和javascript有什么区别? 总结了一下:有以下几点吧: 1.首先从运行环境来说java代码是在JVM上 ...

  7. java 接口函数_Java函数式接口Supplier接口实例详解

    这篇文章主要介绍了Java函数式接口Supplier接口实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 JDK提供了大量常用的函数式接口以丰 ...

  8. java supplier接口_Java函数式接口Supplier接口实例详解

    这篇文章主要介绍了Java函数式接口Supplier接口实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 JDK提供了大量常用的函数式接口以丰 ...

  9. java mod %区别_Java中 % 与Math.floorMod() 区别详解

    %为取余(rem),Math.floorMod()为取模(mod) 取余取模有什么区别呢? 对于整型数a,b来说,取模运算或者取余运算的方法都是: 1.求 整数商: c = a/b; 2.计算模或者余 ...

最新文章

  1. 仅需6步,教你轻易撕掉app开发框架的神秘面纱(2):MVP比MVC更好吗
  2. 前端利器—1—转型JS编程
  3. java demo在哪里下载_[Java教程]Java学习 (一)、下载,配置环境变量,第一个demo...
  4. 941. Valid Mountain Array 有效的山脉数组
  5. mysql myisam 去掉表锁a_MyISAM表锁的解决方案
  6. mysql函数和索引冲突问题_Mysql索引常见问题汇总
  7. 你知道吗…我不知道…你知道吗
  8. linux alsa声卡命令,Linux ALSA声卡驱动之一:ALSA架构简介
  9. 嵌套页面h5怎么调起手机文件_让微信公众号文章排版像网页h5一样高大上
  10. NLTK使用英文词性还原
  11. python unittest断言_python unittest之断言及示例
  12. 三星想抢苹果芯片订单?台积电表示想多了
  13. c 语言输出字符用什么作用是什么,C语言中输出字符串用什么符号
  14. Javascript 监控键盘输入事件
  15. Linux配置java环境变量
  16. python怎么看自己安装的第三方包_安装第三方包查看python版本/第三方包版本
  17. 1412202035-hpu-1003:C语言考试练习题_一元二次方程
  18. 又是一年金九银十!docker搭建ssr
  19. unity动态加载obj文件
  20. The Willpower Instinct

热门文章

  1. 御坂网络(枚举基准,二分图)
  2. 御坂坂的c++学习之路(6)
  3. 御坂坂的C++学习之路(7)
  4. 60项基于深度学习的SLAM顶会开源方案汇总(上篇)
  5. Python 解压rar类型文件
  6. tpc-e mysql_mysql评测工具TPC-C使用
  7. mysql多表查询详解
  8. r720服务器怎么查看硬盘性能,r720服务器如何看配置
  9. Mybatis中,SQLSessionFactoryBuilder使用build方法时做了哪些事?
  10. 怎么将红底照片用PS换成白底